I don't like in memory repositories, I prefer mocking if you need to unit test some code that has dependencies. In my experience it is so useless to maintain updated the test doubles when you have complex logic, on top of that is code that is never going to touch production.
Nowadays, I like to limit my unit tests to pure functions/ methods.
Jhon
commented
Thanks for this article, I agree with this. One small mistake in Using_mocks() method, you meant to pass repo instead of "mock"
I like this approach since it keeps things simple and maintainable. But often people complain that they have to write additional test doubles. I think we need to point out more the advantages on maintainability which have more a mid term focus and do not pay out when writing the very first test.
Btw.: there's a typo in the first code example using the test double. You instantiate the in memory repository in the variable 'repo' but pass it as 'testdouble' to the system under test.
nope
commented
"Your test doesn't need to know this, it only needs to know that the product is stored in the repository. To explain further, here's the test double and revised test" and so on is just smoke in the eyes.
Mock leaking abstraction by exposing internals? Yup. Binds tightly tests to that internals? Partially, it binds to the interface of IRepository. But yeah, you could call that 'internals' since we're testing ProductService (it's not 'internals', it's DEPENDENCIES, but I'll let it slide). Modify "internals" means correct all tests? Yup. That's mocks and interfaces. Plain in the eyes.
But now let's see your test doubles. You wrote i.e. InMemoryProductRepository. You have to implement the whole interface, even if 99% is not used in the test. You have to manually add switches and tricks like AlwaysThrowsWhenStoring. Is it all bound to internals? Oh yes! Since it basically refers to the same interface as the mocks, it also has to refer to methods like 'Store' so it has the same binding to the 'internals' like that was in the previous case.
But there's a difference. Now the 'test double' is bound to that, not the test itself. Ok. Let's say that's a point.
Now, let's look what happens NEXT.
You will have 10, 100, 1000 tests like that. Either you will create test doubles for each test separately (and cry at the duplication), or sooner or later you will try to reuse test doubles. This means that all the SWITCHes and HACKs like AlwaysThrowsWhenStoring will start building up within the implementation of the test double. You will have to mantain, design, plan out, untangle, everything, or you will end up with absolute ball of mud that everyone will be afraid to touch and that everyone will just add more new switches on top of old switches just to ensure that no old tests break.
Ok. So for your 100s test(cases), depending on how much you care about impl of doubles, you either have 30+ slim hardly-reusable test doubles for IRepository, or you have just 1-3-maybe-5 of them, highly-reusable, ugly inside (if they just grew) or nice inside (if you spent time to design&refactor&..). Now let's say something in 'internals' changed. Sure, you don't have to correct 100s of tests. WIth mocks you probably would need to adjust not every single test, but those tests that touch the area that changed. Say, 40 tests. With doubles, you have to correct EVERY single double. You had lots of simple slim doubles? You have a PITA. You had a few reusable hairball/mudball-style test double? You have a PITA - adjusting a mudball in a non-breaking way is hard. Most probably you or your team mate will just add more switches for this one new test and thus grow the mudball. If you had 1-2 nicely designed polished doubles, yes, you won! You just update the double's impl in that 1-2 places, and done! But even with, or especially with nicely designed doubles, if a serious change shows up, you will want to like to adjust its public API (consider 'DidStore'), just to keep it nicely&cleanly&designed, and then you have to get back to correcting related tests. So, win, but kinda partial win.
Now. Let's see those ugly abstraction-leaking mocks. We all know plain utility methods. Factories. Helpers. Extension methods. Code duplication because you have to set up a mock the same way in a few tests? Yeah. Maybe. But no. Just wrap that setup into a utility method and have fun reusing. Lots of such setups? Just wrap it up again, or make a mock factory/helper/whatever so it's easier to find and deduplicate. Repeated Verify/Assert like 'received().store()'? Just wrap that into a method or extension method, again. You probably see where I am going to, so I'll stop.
Every single thing you wrote against mocks is smoke screen. Those issues can be solved with any other typical solutions for those issues. You don't need test doubles to solve them.
Most of contemporary mocking frameworks (like Moq you mentioned) are all about 'behavior' as well. The 'expose internals' part is there jsut because it has to be somewhere. Either in the plain sight, or wrapped up with helper methods for dedupe'ing, or wrapped up in doubles, but it has to be somewhere, and that's because of DEPENDENCIES and INTERFACES like IRepository, not because of mocks/doubles/younameyourtools. And on top of 'behavior' those contemporary mocking frameworks go out of their ways to assist you in SKIPPING what's not needed and on SPECIFYING ONLY WHAT A TEST NEEDS. If you have IRepository with 100 precisely shaped querying methods, your test touches one, your mock will specify just one, and ignore 99. Your test double will throw-not-implemented 99 of them. For godssake, please, no.
Mocks and test doubles are tools. You pick tools depending on the case. You have wide, complex interface, pick mocking framework and trim. You have dead simple interface, make a test double. You like writing classes and boiler place, write test doubles. You like lambdas and combining simple functions into larger blocks, pick mocks. Your nice and coherent test double detoriated into several loose blocks, each used in 1 or 2 places? Split it into short mocks written on-site-of-use. Your simple one-liner mock grew with captures and semi-persistent temporary storage so pair of methods like Add/Find actually kinda work? Scrap it and convert to a test double.
Do NOT "Prefer test-doubles over mocking frameworks".
Instead, consider mocks or test doubles equal and choose them where they make more sense and are easier to mantain in the foreseeable future.
James Moore
commented
If you find yourself reaching for tools like this, stop. You need a full end to end test that actually hits the real database or service.
Marc
commented
I like that more people are talking thoughtfully about friction points in unit tests.
Some devs & teams might already have a lot of experience with mocking frameworks. And at a point, simple mocking code might be simpler to read across a codebase than many new and unique test double classes.
Thinking out loud, I wonder if it's better to use custom test doubles for the most complicated parts of the codebase rather than as a default approach.
8 comments on this page
jofla
I don't like in memory repositories, I prefer mocking if you need to unit test some code that has dependencies. In my experience it is so useless to maintain updated the test doubles when you have complex logic, on top of that is code that is never going to touch production.
Nowadays, I like to limit my unit tests to pure functions/ methods.
Jhon
Thanks for this article, I agree with this. One small mistake in Using_mocks() method, you meant to pass repo instead of "mock"
Jason Bock
Your post's motivated me to make a video on my views of test doubles and mocks - https://www.youtube.com/watch?v=n__1wXwljtk
Krishna
Thanks for the write up, it was illuminating!
Alex Rampp
I like this approach since it keeps things simple and maintainable. But often people complain that they have to write additional test doubles. I think we need to point out more the advantages on maintainability which have more a mid term focus and do not pay out when writing the very first test.
Btw.: there's a typo in the first code example using the test double. You instantiate the in memory repository in the variable 'repo' but pass it as 'testdouble' to the system under test.
nope
"Your test doesn't need to know this, it only needs to know that the product is stored in the repository. To explain further, here's the test double and revised test" and so on is just smoke in the eyes.
Mock leaking abstraction by exposing internals? Yup. Binds tightly tests to that internals? Partially, it binds to the interface of IRepository. But yeah, you could call that 'internals' since we're testing ProductService (it's not 'internals', it's DEPENDENCIES, but I'll let it slide). Modify "internals" means correct all tests? Yup. That's mocks and interfaces. Plain in the eyes.
But now let's see your test doubles. You wrote i.e. InMemoryProductRepository. You have to implement the whole interface, even if 99% is not used in the test. You have to manually add switches and tricks like AlwaysThrowsWhenStoring. Is it all bound to internals? Oh yes! Since it basically refers to the same interface as the mocks, it also has to refer to methods like 'Store' so it has the same binding to the 'internals' like that was in the previous case.
But there's a difference. Now the 'test double' is bound to that, not the test itself. Ok. Let's say that's a point.
Now, let's look what happens NEXT.
You will have 10, 100, 1000 tests like that. Either you will create test doubles for each test separately (and cry at the duplication), or sooner or later you will try to reuse test doubles. This means that all the SWITCHes and HACKs like AlwaysThrowsWhenStoring will start building up within the implementation of the test double. You will have to mantain, design, plan out, untangle, everything, or you will end up with absolute ball of mud that everyone will be afraid to touch and that everyone will just add more new switches on top of old switches just to ensure that no old tests break.
Ok. So for your 100s test(cases), depending on how much you care about impl of doubles, you either have 30+ slim hardly-reusable test doubles for IRepository, or you have just 1-3-maybe-5 of them, highly-reusable, ugly inside (if they just grew) or nice inside (if you spent time to design&refactor&..). Now let's say something in 'internals' changed. Sure, you don't have to correct 100s of tests. WIth mocks you probably would need to adjust not every single test, but those tests that touch the area that changed. Say, 40 tests. With doubles, you have to correct EVERY single double. You had lots of simple slim doubles? You have a PITA. You had a few reusable hairball/mudball-style test double? You have a PITA - adjusting a mudball in a non-breaking way is hard. Most probably you or your team mate will just add more switches for this one new test and thus grow the mudball. If you had 1-2 nicely designed polished doubles, yes, you won! You just update the double's impl in that 1-2 places, and done! But even with, or especially with nicely designed doubles, if a serious change shows up, you will want to like to adjust its public API (consider 'DidStore'), just to keep it nicely&cleanly&designed, and then you have to get back to correcting related tests. So, win, but kinda partial win.
Now. Let's see those ugly abstraction-leaking mocks. We all know plain utility methods. Factories. Helpers. Extension methods. Code duplication because you have to set up a mock the same way in a few tests? Yeah. Maybe. But no. Just wrap that setup into a utility method and have fun reusing. Lots of such setups? Just wrap it up again, or make a mock factory/helper/whatever so it's easier to find and deduplicate. Repeated Verify/Assert like 'received().store()'? Just wrap that into a method or extension method, again. You probably see where I am going to, so I'll stop.
Every single thing you wrote against mocks is smoke screen. Those issues can be solved with any other typical solutions for those issues. You don't need test doubles to solve them.
Most of contemporary mocking frameworks (like Moq you mentioned) are all about 'behavior' as well. The 'expose internals' part is there jsut because it has to be somewhere. Either in the plain sight, or wrapped up with helper methods for dedupe'ing, or wrapped up in doubles, but it has to be somewhere, and that's because of DEPENDENCIES and INTERFACES like IRepository, not because of mocks/doubles/younameyourtools. And on top of 'behavior' those contemporary mocking frameworks go out of their ways to assist you in SKIPPING what's not needed and on SPECIFYING ONLY WHAT A TEST NEEDS. If you have IRepository with 100 precisely shaped querying methods, your test touches one, your mock will specify just one, and ignore 99. Your test double will throw-not-implemented 99 of them. For godssake, please, no.
Mocks and test doubles are tools. You pick tools depending on the case. You have wide, complex interface, pick mocking framework and trim. You have dead simple interface, make a test double. You like writing classes and boiler place, write test doubles. You like lambdas and combining simple functions into larger blocks, pick mocks. Your nice and coherent test double detoriated into several loose blocks, each used in 1 or 2 places? Split it into short mocks written on-site-of-use. Your simple one-liner mock grew with captures and semi-persistent temporary storage so pair of methods like Add/Find actually kinda work? Scrap it and convert to a test double.
Do NOT "Prefer test-doubles over mocking frameworks".
Instead, consider mocks or test doubles equal and choose them where they make more sense and are easier to mantain in the foreseeable future.
James Moore
If you find yourself reaching for tools like this, stop. You need a full end to end test that actually hits the real database or service.
Marc
I like that more people are talking thoughtfully about friction points in unit tests.
Some devs & teams might already have a lot of experience with mocking frameworks. And at a point, simple mocking code might be simpler to read across a codebase than many new and unique test double classes.
Thinking out loud, I wonder if it's better to use custom test doubles for the most complicated parts of the codebase rather than as a default approach.