I see this type of mock-heavy testing in lots of places. I used to do the same thing too, but I found that it basically just tightly couples your test to your implementation details. The whole purpose of unit tests is to be able to refactor things with confidence that your tests will still pass. If you so closely tie your test to your implementation you lose that.
In the example, the tests now really care about how data is being retrieved from the database. Instead, I'd use an in-memory database (or even a Dockerized one). That would then also test that the right queries are being done, but you could still refactor the internals of UserServiceImpl (terrible name btw) and your tests wouldn't fail.
I tend to do mock-heavy testing in Java projects, and it has burned me several times. You write mock tests along your architectural boundaries, but if those need to change due to a deeper refactoring, all your tests need to get refactored as well.
Mock testing essentially tests expected side effects. A more powerful concept is the use of pure functions wherever possible, so that your tests compare input/output pairs, instead of long, difficult to maintain and sometimes non-exhaustive lists of expected side effects.
Does anybody know how one can replace Services, Controllers, Presenters and other such imperative mediator objects with a more functional approach? I'm just speculating, but that should make test maintenance easier.
You might be interested in the "free monad" approach used occasionally in scala and haskell. It separates the effectful operations from the effects by first building a data structure that describes what to do and then interpreting that data structure. The key is that you can interpret the data structure in different ways. That is, one interpreter for production and another for testing. The advantage this has over mocking is that the business logic becomes a collection of pure functions.
Take a look at Gary Bernhardt's talk "Boundaries"[1]. It touches on this very topic and was quite the eye-opener for me (having experienced all the same issues you've described).
Monads. Possibly the Free Monad. Make your business logic pure logic that computes commands, and separate the computation of the command from its execution, then you can use a different execution to test your command chains. http://michaelxavier.net/posts/2014-04-27-Cool-Idea-Free-Mon... has a basic example, but it works well for replacing your services/controllers/what-have-you too.
I find that you can keep with a class-based approach if you utilise your IOC container in your tests, and only swap out side-effectful classes (ORM, remote service clients, etc) with mocks. That way you don't need to concern yourself with the dependencies of the classes you're testing so you can refactor the underlying relationships and objects with impunity as long as you don't change the interfaces of the classes you're testing.
I don't know if I asked my question as clearly as possible. Mocking your DB, the file system, the remote API, etc are all a given when doing tests. My question was aimed at trying to find out if it's feasible to replace the imperative Presenter/Service/etc patterns with a more functional approach.
What I'd like to see is testing only the returned value of a function, as opposed to calling a void function and checking whether the injected mocks got the proper method invocations, i.e. whether the desired side effects occurred.
if you're calling a void function then by definition you're calling a function that does nothing but side effects or calling off to other classes. In the latter case, they can be good candidates for refactoring in my experience. A function that does something but doesn't either produce mockable side-effects or return information may be a code smell.
A function can return void and still throw an exception. (Like one I wrote today, "insert row into table", which throws on failure and return nothing on success.)
A bunch of functions that all return void all being called in a row, though? That's a much smellier smell. What happens if their order gets mixed up, for example.
Testing that function would be a matter of mocking out the database with a class that can emulate the type of failure that would trigger your exception.
it's almost more worthwhile than testing your happy path - when things go wrong you want to have guarantees that they'll be handled correctly and not kill your server.
"tests now really care about how data is being retrieved from the database".
I'm not sure this correct this case. He mocks the repo. The interface for the repo cares about the intention, the implementation cares about the "how". If your creating a user, you always going to want a "add" to persistent store intention. You don't really care how the add to persistent store is done though, or what persistent store. You just care the service intended to do so.
I have an idea that you if your performing a command, you should mock. The whole points of a command is to coordinate subsystems to perform an action. So you need to check if the right methods were called.
If your performing a query you should stub or in memory db.
> I found that it basically just tightly couples your test to your implementation details
Compilation in strictest form of TDD is the first failing test that you run. Catching this has advantages, such as knowledge and contemplation of altered ABI.
Your interface refactoring could also originate from your tests. Alter the code in your test (even the mock interface) until it is clean at that usage site, and the compile/test run until everything passes.
This is undesirable in my book.
Mere refactoring should just check all boxes in test suite. If I changed tests as well, how would I be sure no functionality changed? Tests did not serve their's purpouse in the end
This whole discussion has been about an "interface refactoring". I.e. a change to internal interfaces so obviously anything that sees that interface is subject to change.
Now it is true that some interfaces are more stable than
others, and you can make a good argument that testing
shouldn't be done at boundaries that are likely to be
unstable.
A test should care about: input, "do something", output.
Then you can refactor the heck out of "do something" and your test (which is ideally documenting the use cases you need to support) doesn't have to change at all.
For me, preferring stubs (dummy implementations of your external dependencies), when possible/appropriate, is a good way to avoid "your test is just a repetition of which method calls your implementation makes":
> The whole purpose of unit tests is to be able to refactor things with confidence that your tests will still pass.
(assuming the second part of that statement should have been "that your program will still be correct")
I'm back and forth on my agreement/disagreement on this one. One of the major purposes of tests is to have confidence in refactoring, but it isn't the _only_ purpose. Beyond that though, I'd say that unit tests probably aren't the right tool for the job when we're talking about refactoring. The purpose of unit tests is to verify that the unit of code is working in isolation. Integration tests are much better suited for verifying correctness when refactoring. Any good test suite should have a healthy balance of unit tests and integration tests. And, when I say integration tests, I mean whatever you want to call them -- tests that check the system as a whole.
I don't necessarily disagree with you though. Mocks aren't perfect, and they have their own host of problems. I've run into a number of passing tests that should have never been that way because of mocks. It happens. The best way to combat this is to use mocks more sparingly, and have feature tests for many of these cases.
I completely agree with the idea of an "in memory database". I use Entity Framework. I mock the EF context to return regular in memory Lists of objects in unit tests the linq -> sql translation is replace with linq to objects.
I think I would reverse the question. Why does there have to be a UserService interface? If it only exists in order to be mocked in yet another place of the code you've added even more unnecessary layering in your codebase.
In the example, the tests now really care about how data is being retrieved from the database. Instead, I'd use an in-memory database (or even a Dockerized one). That would then also test that the right queries are being done, but you could still refactor the internals of UserServiceImpl (terrible name btw) and your tests wouldn't fail.