6 Misconceptions About TDD – Part 5. Mocks, Mocks Everywhere!
Another part of our guide to the TDD cycle – this time, we take a closer look at mocks in testing.
Welcome to the next part of our guide to the TDD cycle! Let’s dive in.
An essential part of the TDD cycle methodology is writing tests. Many of our features depend of other (external) services that we couldn’t use in our test. Examples of these services are: sending emails, oauth2 authentication or gathering actual exchange rates.
Why can’t we use them? Because investigating the results is hard and/or we must pay for using them. Or because using a real implementation would increase the time of execution dramatically.
In such cases, if we want to be able to test these features we need to declare a contract (e.g. as interface) between these functionalities. That’s how we can inject a specific implementation for testing purposes.
Introducing mocks in the TDD cycle
When talking about testing abuse, many developers use the word “mock” meaning an implementation which is substitute for the real implementation for testing purposes.
According to the terminology used by Gerard Meszaros in XUnit Test Patterns, a mock is one of the few options for handling the injection of a specific implementation for testing purposes. Meszarosa uses term “doubler” as a general term describing this mechanism. This is an analogy to the world of movies where the doubler of an actor plays scenes which require some special skills or are dangerous.
Aside from mocks, there are also dummies, fakes, stubs and spices (based on the XUnit Test Pattern, there are also some alternatives names).
Here’s a short overview of these doublers.
Dummy
Representing doublers that we only need for a fulfilled signature of the function/methods in test time. They’re useful when creating of required object is too complicated. The interaction between the dummy and the rest of the testing code is none or negligible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | interface FacebookService { getLoginUrl: () => string; } class AuthService { facebookService: FacebookService; constructor(facebookService: FacebookService) { this.facebookService = facebookService; } authorize(email: string, password: string): boolean { return ( ); } getFacebookLogin() { return this.facebookService.getLoginUrl(); } } test("sign in by email and password", () => { const fakeFacebookService = { getLoginUrl: () => "" }; const authService = new AuthService(fakeFacebookService); expect( ).toEqual(true); }); |
Fake
This is a real but simplified implementation of the dependency functionality we need for testing. It’s used when the double feature is hard to the modeled process with stub (or when it’s impossible) or when the real implementation is too slow for testing. Yeah, obviously an example is using a database in the memory as a substitute for (NO)Sql database.
1 2 3 4 5 6 7 8 9 10 11 | before(() => { const dbConnection = new sqlLite("..."); const userRepository = new UserRepository(dbConnection); }); test('sign in by email and password', () => { const fakeFacebookService = { getLoginUrl: () => "" }; const authService = new AuthService(fakeFacebookService, userRepository); }); |
Spy
Spy is an extended version of stub, which stores information about how they are executed (for example, which parameters are used). We need them when we want to test the indirect state of a functionality which isn’t available outside the functionality that we’re going to test.
1 2 3 4 5 6 7 8 9 10 11 12 13 | before(() => { const dbConnection = new sqlLite('...'); const userRepository = new UserRepository(dbConnection); sion.spy(userRepository, 'findByEmail'); }); test('sign in by email and password', () => { const fakeFacebookService = { getLoginUrl: () => '' }; const authService = new AuthService(fakeFacebookService, userRepository); expect(userRepository.findByEmail.calledOnce).to.equal(true); }); |
Mock
These doublers are used to describe the expected interaction between the system under test and them provide a mechanism to verify if this interaction occurred. It’s the only one that represents the behavior testing of feature.
1 2 3 4 5 6 7 8 9 10 | test('retrive facebook url', () => { const fakeUserRepository = { findByEmail: () => {} }; const mockFacebookService = sion .mock(FacebookService) .expect('getLoginUrl') .once(); const authService = new AuthService(mockFacebookService, fakeUserRepository); authService.getLoginUrl(); expect(mockFacebookService.verify()).not.toThrow(); }); |
For more details about doublers, please check XUnit Test Patterns by Gerard Meszaros.
Classical vs. mockist TDD
When using the first four types of doublers, we focus on testing the state of system under test. On the other hand, testing using mocks concentrates on the behaviors and interactions between features.
Both ways (classical and mockist) have their advantages and disadvantages. When we use the mockist way, we need to initialize only the direct collaborators of the system we want to test.
Advantages of classical testing
In the classical way, we need to initialize also the collaborators of collaborators, and so forth. A bug in one place will not propagate to other tests. When focusing on the testing state, we are using the real implementation whenever we can. However, finding the source of the bug is much more time-consuming.
The greatest advantage of using classical way of testing is the fact that our unit tests are also mini integration tests. As I mentioned before, we are using the real implementation. The second important thing is the fact that classical testing is low coupling to the real implementation then mockist. We are focusing on the expected state which is less susceptible to changes then the changes in interfaces which is strongly coupled to mockist tests.
Based on those advantages and disadvantages we are more likely to use classical/state testing in our daily work. But the decision of which path to choose should go to the developer or the development team.
In many cases, it’s natural or more obvious which type of test needs to be used to test features easily. For example, to test a features like login or sending an email I would use a mock (I expect that some action occurs). But for calculating a discount for an order, I’d prefer the classic way (because I expect to get some values).
The agenda of the article series about the TDD cycle “6 Misconceptions about TDD” is the following:
- TDD brings little business value and isn’t worth it
- We all understand the key laws of TDD in the same way
- TDD cycle can be neglected
- There is one right granularity of steps in TDD
- Mocks, mocks everywhere!
- Tests loosely coupled with code are reliable
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!