May 6, 2017

...Learn TDD with Codemanship

Not All Test Doubles Make Test Code Brittle.

Much talk out there in Interweb-land about when to use test doubles, when not to use test doubles, and when to confuse mocks with stubs (which almost every commentator seems to).

Bob C. Martin blogs about how he uses test doubles sparingly, and makes a good case for avoiding the very real danger of "over-mocking", where all your unit tests expose internal details of interactions between the object they're testing and its collaborators. This can indeed lead to brittle test code that has to be rewritten often as the design evolves.

But mocks are only one kind of test double, and they definitely have their place. And let's also not confuse mock objects with mocking frameworks. Just because we created it using a mocking tool, that doesn't necessarily mean it's a mock object.

I'm always as clear as I can be that a mock object is one that's used to test an interaction with a collaborator; one that allows us to write a test that fails when the interaction doesn't happen. They're a tool for designing interfaces, really. And you don't need a mocking framework to write mock objects.

I, too, use mock objects sparingly. Typically, for two reasons:

1. Because the object being interacted with has direct external dependencies (e.g. a database) that I don't want to include in the execution of the unit test

2. Because the object being interacted with doesn't exist yet - in terms of an implementation. "Fake it 'til you make it."

In both cases, I'm clear in my own mind that it's only a mock object if the test is specifically about the interaction. A test double that pretends to fetch data from a SQL database is a stub, not a mock. Test doubles that provide test data are stubs. Test doubles that allow us to test interactions are mocks.

Mocks necessarily require our tests to specify an internal interaction. What method should be invoked? What parameter values shoud be passed? I tend to ask those kinds of questions less often.

Stubs don't necessarily have to expose those internal details in the test code. Knowledge of how the object under test asks for the data can be encapsulated inside a general-purpose stub implementation and left out of the actual test itself.

In this example, I'm stubbing an object that knows about video library members who expressed in interest in newly added titles that match a certain string. This is one of those "fake it 'til you make it" examples. We haven't built the component that manages those lists yet.

The stub is parameterised, and we pass in the test data to its constructor. It's not revealed in the test how EmailAlert gets that data from the stub.

This stub code, of course, is test code, too. But using this technique, we don't have to repeat the knowledge of how the stub provides its data to the object under test. So if that detail changes, we only need to change it in one place.

Another thing I do sometimes is use a mocking framework to create dummies of objects where we're not interested in the interaction, and it provides not test data, but it needs to be there and it needs an object indentity for our test.

In this example, Title doesn't need to be a real implementation. We're not interested in any interactions with Title, but we do need to know if it's in the library at the end. This test code doesn't expose any internal details of how Library.donate() works.

If you check out the code for my array combiner spike, you'll notice that there's no use of test doubles at all. This is because of its architectural nature. There are no external dependencies: no database, no use of web services, etc. And there are no components of the design that were so complex that I felt the need to fake them until I made them.

So, to summarise, in my experience over-reliance on mocks can bake in a bad design. (Although, used wisely, they can help us produce a much cleaner design, so there's a balance to be struck.) But I thought I should just qualify "test double", because not all uses of them have that same risk.

Posted 3 years, 4 months ago on May 6, 2017