A lot of developers are very vague when they name their test doubles which makes tests needlessly more difficult to understand.
In conversation there’s also often ambiguities which cause problems
I overheard on a podcast
Have you ever used a mock where you make assertions on the call?
This post will hopefully illuminate why that sentence is a bit silly!
By making a little more effort to be precise with your naming, you can communicate the intent of your tests and the design of your system more effectively
Quick primer on dependencies
To separate concerns, we often make objects that will “depend” on other objects.
We “inject” or “pass in” these dependencies when we construct them, so they can do a job.
This is called Dependency Injection.
Example
A ShoppingCart
sends an email when its sendOrder
method is called
We don’t want ShoppingCart
to have to know how to send emails as well as everything else so we give it an Emailer
when we create it.
new ShoppingCart(new MailgunEmailer())
It can then use the emailer without having to know how emails are sent, and you have your separation of concerns.
sendOrder() {
// interesting domain logic
this.emailer.send(confirmation)
}
This approach has implications for our tests.
With tests, we prefer not to use real objects for dependencies because we want to be able to control them, so we use something different
- It would not be desirable to send a real email every time a test is run.
- We sometimes depend on things that are slow, but we like our tests to be fast because feedback loops are extremely important
- It would be hard for us to exercise failure scenarios using real dependencies. How do you get a 3rd party email service to deliberately fail?
Use “Test Doubles”
Not specifically mocks
A test double is what you inject into an "object under test" (OUT) to control a dependency.
Like a stunt double!
Using the precise words for test doubles helps reveal intent and documents your design more effectively
It’s not about being big and clever, it helps you communicate unambiguously. Software development is a team-sport so clear communication will help your team work better.
The different kinds of test doubles
Dummies
Sometimes you have arguments to functions that you don’t care about. Dummies are the arguments you pass in.
const dummyLogger = jest.fn()
Stubs
Some objects will query dependencies for data.
Stub the dependency
const stubUserService = jest.fn(() => 'Miss Jones')
Spies
Sometimes you want to call a method (or more abstractly send a command) to a dependency.
It can be hard to tell in a test this happened from the outside as it’s an internal detail.
If you want to verify these commands, use a spy.
const spyEmailSender = jest.fn()
// do some stuff
expect(spyEmailSender.mock.calls.length).toBe(1)
Fakes
Usually a “real” implementation but constrained.
Like an in-memory database.
Not as popular anymore due to things like Docker which let you spin up local versions of databases without the risk of there being differences between a fake and a real implementation.
Mocks
Mocks are a kind of test double!
Mocks are precise and encapsulate the detail of your interactions more explicitly.
You describe upfront what calls you expect on your test double
- Order
- Arguments
- Specific return values
If the object under test doesn’t call things as expected the mock will error.
So returning to
Have you ever used a mock where you make assertions on the call?
What is being described is just a mock. If you're not worried about the calls, you're talking about a stub.
Why “mocking too much is bad”
The risk is your tests become needlessly precise and coupled to implementation detail.
This is also true of spies as they have a similar nature.
If you're overly specific with your mocks, when you want to refactor things you may find tests failing for annoying reasons that have nothing to do with behaviour.
Why “having too many test doubles is bad”
Listen to your tests if they cause you pain
If you have many test doubles in a test that means your OUT has many dependencies.
Does that sound right to you? It shouldn’t and it means you probably need to take a look at your design.
An anecdote
Yesterday I reviewed a test which had a double named
getterFn
I had to spend some time reading the test to understand what was going on, it turns out it should've been called:
spyCSVFetcher
Once this was ascertained it turned out the writer had actually neglected to spy on the number of calls, which was actually a core part of what was supposed to be tested.
If the double had been named correctly in the first place (which is very little effort) I would know to look for assertions on the way it was called to help me understand what the intent of the code is. Instead, it took some investigation.
Summary
Consistently using the recognised names for test doubles will help you be precise when communicating with other developers. It's a low-effort way to improve the way you work.
For instance if you name something a dummy
rather than a mock
the readers of your tests can forget about that particular test double and focus on the important collaborators.
Tests should act as documentation so describing your test doubles clearly will help reveal the design of the test for maintainers.
Quiz
- What does a test with lots of doubles tell you?
- How do you improve the design of a test with many test doubles?
- Name all the kinds of test doubles
- How is a mock different from a spy?
Further reading
- Martin Fowler's post "Mocks aren't stubs" goes into this topic in great detail.
- Learn Go with tests: Dependency Injection
Top comments (0)