I mean I am a functional programmer so i naturally make code pure until the edges anyway. In a functional language I will just replace the edge function call with a fake version.
But the problem with mocking, is you couple your objects to one another. You expect certain methods to be called. With certain parameters.
What if you want to add in extra layer, or remove a layer, or add a helper. All your tests have now been broken yet the overall behaviour has not changed.
You also have issues around mocks not acutally setup to behave like the real object.
If your object under test sends a null it may pass but the real thing may fail.
Instead create an architectural boundry with a clear public interface. Exercise the interfaces in tests and then check for results on the otherside of the boundary. You might do that with a mock or a fake.
You are now free to refactor from the start to finish of that architectural boundry without breaking tests.
This is just fancy talk for only testing your public API. But defining those boundries and public interfaces is where the skill is.
If your building a web app using hexagonal architecture, I might say drive your primary ports from your tests, mock and fake your secondary ports.
If you expect some group of objects to used in multiple places like a library I will test those as if they're a boundry to. For example If I've built money exchange rate module.
> Instead create an architectural boundry with a clear public interface. Exercise the interfaces in tests and then check for results on the otherside of the boundary. You might do that with a mock or a fake.
I think mockist (test-doublist really) TDDers would suggest that the architectural boundaries derived from the design pressure are the "correct" public interfaces you're describing here, they just often happen to coincide with class (or your favourite languages equivalent) boundaries.
The pattern often ends up with many one-function role interfaces, orchestrated by collaborators down the dependency tree until you hit value structures, pure functions or external integrations at the edge of the system. In many ways, mockist TDD is a gateway to functional programming.
Hm, yes and no. I do feel that mockist TDD is still heavily emphasising the OOP idea (more in the original sense than in the Java sense) of having separate "collaborators" with their own internal state and side effects that exchange messages (in a way similar to the Actor pattern).
A functional approach (at least in a pure functional language) wouldn't necessarily emphasise this sort of interaction pattern between independent components and try to isolate state and side effects much more.
If you do functional programming, obviously many OOP techniques just don't apply. This is more of a difference in philosophy. I'm sympathetic to functional programming, and do try to use immutability, value objects, referential transparency, explicit state handling etc., whenever possible, but I'm still constrained by the languages, frameworks and teams that I work with, and I think many others are as well.
Of course, the question whether we should all just program in Haskell (or lisp, or erlang, ...) can be debated, but for a variety of reasons that is not currently the case, so I think mocks are still a valid answer for OOP, if (!) you use them correctly (and I agree that many may be too cavalier about mocking).
But to answer your question about your "extra layer": If your code is written in a domain driven style, then potentially adding in a new layer should be considered a change in behaviour, so changing the tests makes sense. If it's purely a technical thing, then there are IMHO often ways of not exposing them to surrounding code. As an example, if you're introducing some e.g. logging layer to your BillingService, instead of pasting in that layer in the original code that calls the billing_service, you could decorate your BillingService with a logging wrapper class and just change the injected dependency. Nothing about the tests using the billing_service would have to change. This is a stupid example, but I hope it gets the point across.
> What if you want to add in extra layer, or remove a layer, or add a helper.
You make the extra layer implement the same interface and delegate to the original object. Classes don't depend directly on other implementation classes in this style.
But the problem with mocking, is you couple your objects to one another. You expect certain methods to be called. With certain parameters.
What if you want to add in extra layer, or remove a layer, or add a helper. All your tests have now been broken yet the overall behaviour has not changed.
You also have issues around mocks not acutally setup to behave like the real object.
If your object under test sends a null it may pass but the real thing may fail.
Instead create an architectural boundry with a clear public interface. Exercise the interfaces in tests and then check for results on the otherside of the boundary. You might do that with a mock or a fake.
You are now free to refactor from the start to finish of that architectural boundry without breaking tests.
This is just fancy talk for only testing your public API. But defining those boundries and public interfaces is where the skill is.
If your building a web app using hexagonal architecture, I might say drive your primary ports from your tests, mock and fake your secondary ports.
If you expect some group of objects to used in multiple places like a library I will test those as if they're a boundry to. For example If I've built money exchange rate module.