Some days, I wish less developers knew that testing mocks and stubs even existed. What started out as a specific tool to solve a specific problem has become the crutch of so many poorly written unit tests.
understanding interfaces
Just because Ruby lacks interfaces doesn’t mean you shouldn’t think about their benefits in your code and tests. Have you ever seen a Ruby class where every method is public just to make things “easier to test”? I have.
Unit tests are consumers of your class interface. A well written unit test should have no awareness of the internal workings of your class. The second you start stubbing out private methods or putting mocks in place of your domain models, you’ve added some more assumptions to the moving pieces of your object. Your tests have become too smart. By stubbing out private methods in your class, you’re basically adding another interface contact point that must be maintained. Changing that private method? Better update your stubs, or you’ll have a lot of red when things change (or worse yet, too much green where there should have been more red).
If another developer got mad at you because you renamed a private method they were calling without telling you, you’d tell them to bug off. Private methods are implementation details, right? Why shouldn’t we have the same attitudes with our tests? Don’t put support braces around things that should be able to stand on their own.
common objection 1: long setups
“But Blake, I don’t want to have to setup the entire world just to test one sliver of functionality”.
My answer to this protest is: why are you writing code that depends on the entire world being setup in the first place? Unit tests should be small, isolated, and purely functional in nature. The moment you start messing with the database and trying to run through an entire user behavior is usually a sign that it’s time to stop writing unit tests and start writing integration tests. If you’re testing pure functions (calculations, values in -> values out) unit tests work swimingly - otherwise, reach for something else.
Let me say this loud and clear: stubs should not be used as a crutch to avoid complex test setups. Once you start using the stub crutch, you’re going to have to add more mess just to keep things passing. It’s a slippery slope, and most of the time you’re going to get more long-term value by just ditching the unit test and going straight to a full-blown integration test.
common objection 2: testing in isolation
“But Blake, my test doesn’t care about what this line is doing, it only cares about this one. I want to isolate this functionality”
It sounds like you’ve already almost answered your own problem! The solution here is not to start going stub happy and mock out whole world except just the line in question - following this line of thinking to its logical end and you’re almost to the point of testing your programming language itself! Remember that everytime you stub you’re putting an interface expectation in place. Using a stub just to quarantine off sections of code that are doing too much is going to get you in to a nasty mess. Applying the single responsibility principle can save you a lot of headache in the long term.
when should I stub/mock?
There are definitely cases where mocking and stubbing make a lot of sense, and will help avoid incidental complexity in your tests. Places where stubbing make a lot of sense include:
- Filesystem access
- Expensive network calls
- Third party API integration
… and plenty of other examples. A good question to ask yourself when considering applying a stub is, “Am I crossing an interface boundary that’s not part my application’s responsibility?” If you’re writing against a third party API, you don’t have control over the implementation details of the API, but you do have control over the way your code interacts with the API interfaces. Another good question to ask yourself is, “How stable is this API? Is it likely to change often?” If you’re stubbing out the API to your filesystem, the interface should hopefully be pretty stable.
As an added bonus, these kinds of touchpoints do benefit from having integration tests to ensure that the expected interface contracts are being upheld - it’s extremely helpful to know when stub expectations need to be modified in the code you’re writing and good integration tests can help with this.
final thoughts
These days, I’m seeing m ore and more value on the extremes of the testing continuum: I like my test suite to have lots of (pure) unit tests, lots of integration tests and very few things in between. You should either be testing a pure function that has little to no state, or you should be setting up a full blown test that exercises the end-to-end expectations of an entire system behavior.
Stubs and mocks are terrific tools for drawing interface boundaries and ensuring that your code conforms to those interop points. Overuse of stubs in the face of ill-defined or poorly thought out code contracts is like wrapping your code in a spider web that will entangle you the more you move. Used sparingly, and in the right situations a stub will help you get the job done and focus on solving the problems you’re really trying to solve.