There are lots of different way to fake behaviour in tests - you've got your mocks, your spies, your stubs, your doubles etc. They are all (subtly) different. So let me say, right off the bat - I can never remember which is which. I'm going to use these terms interchangeably, and in some cases - wrongly. If you think this stuff is important, please let me know in the comments.
A colleague of mine invited Tim Mackinnon to an informal drink at our office one day. He introduced Tim is “the co-discoverer of mock objects”.
I felt that it was only common courtesy that I read the man’s work before meeting him.
Reading how Tim and his colleagues used mock objects to make the tests guide the design of their application was eye opening.
How it works (as I understand it)
In very short – the process, termed “needs-driven development” (but I call it “mock-driven development” because, why not?), is this:
- Tests for a particular functionality are written first, before implementation. So far this is good ol’ TDD.
- Unlike ‘classic’ TDD, we don’t make the test pass by implementing the functionality. Instead, we discover what the code under test needs from the ‘outside world’ to make the test pass.
Meaning – what are the dependencies that the code under test needs to have in order to do its job. - We then use test fakes to simulate those dependencies, without implementing any of them.
An example from the above article
The authors set out to implement a cache class that falls back to loading objects from the database, if they are not cached.
The first test is: Given that the cache is empty, when key A is requested, then object A is returned.
However, rather than being satisfied with black-box testing such as expect(cache.get('keyA')).to eq(objA)
, the authors stopped to think not only about the interface (tested above), but also about the design.
They realized that the cache object would need to fetch the objects from storage (as they’re not cached). They identified “fetching from storage” as a distinct responsibility. This responsibility can (and should) be implemented by a separate object.
They named this object “object loader”. And they’ve coded this into their test: expect(object_loader).to receive(:fetch).with('keyA').and_return(objA)
That meant that they were able to get this test to pass without worrying about how to actually fetch objects from storage. All they had to do was find a way to get a fake object loader into their cache class.
The complete test case looks something like this –
it 'loads an object from storage when the cache is empty' do
object_loader = double('object loader')
expect(object_loader).to receive(:fetch).with('keyA').and_return(objA)
cache = Cache.new(object_loader)
expect(cache.get('keyA')).to eq(objA)
end
Then, they were able to continue focusing on further functionality of the cache (like caching, eviction policies etc.), without getting sidetracked.
I recommend reading the full article – it’s not nearly as academical and scary as it looks.
A new(ish) way to look at things
I never used test fakes like that – I’ve always used them after the fact.
Either when writing tests after I’ve written implementation (insert wrist-slap here), or as a way to speed up slow tests by faking-out the slow bits.
When writing code, I’m always trying to think as ‘dumbly’ as possible. As in – when writing class A that uses class B, I pretend like I have no idea how class B works. Looking only at B’s interface helps me decouple A from B, and helps me detect any leaky abstractions that B may expose.
That’s why the ‘mock driven development’ approach suggested by Tim et al., appealed to me. The notion of “I know I need an X here, but I don’t want to actually think about X for now” is exactly how I like to think.
A simple example
I’ll describe how I used this process to implement a repository that uses AWS S3 as its backing storage.
The required behaviour was: take an object as input, and write it to an S3 bucket.
I started out by defining the steps in the process: serialize the object to string, connect to an S3 bucket, write string to bucket.
By identifying the different steps, the design became apparent.
Using fakes, I was able to write tests that verified these steps:
it 'uses a serializer to serialize the given object' do
input = MyObject.new
serializer = double('serializer')
expect(serializer).to receive(:serialize).with(input)
described_class.new(serializer).save(input)
end
(Note: I’m using dependency injection to enable me to stub-out the serializer object.)
The only thing I know after writing this test is that the input is serialized using something called `serializer`, that has a `serialize` method. That’s the first step, and I can write some code to implement that.
Now to implement the next step: connect to an S3 bucket
it 'connects to an S3 bucket' do
bucket_factory = double('bucket factory')
expect(bucket_factory).to receive(:get_bucket)
input = MyObject.new
serializer = double('serializer')
allow(serializer).to receive(:serialize)
described_class.new(serializer, bucket_factory).save(input)
end
Here I realized that connecting to an S3 bucket is its own responsibility, and opted for a factory design pattern. Again, I don’t care at this point how this factory object works.
This was not my original design, though – using a factory was not my first thought.
I actually started by writing a test that verified that the repository connected to S3, by stubbing some of the AWS SDK classes.
However, I found the test setup too verbose and complex. That made me realize that the implementation would be too.
A verbose and complex implementation suggests that there’s extra responsibilities to tease out.
(Also – they say “don’t mock what you don’t own”. So, in any case, it would’ve been better to wrap the AWS-specific stuff in a wrapper class and mock that wrapper class)
I realized all that without writing a single line of implementation code. All it took to change my design was changing a few lines of test code; it cost me nothing.
By abstracting-away the interaction with AWS I made my life much simpler, and I was able to stay focused on the original steps defined above.
Later, when I went to implement the factory class, my life was pretty simple, again. I could ignore everything – serialization, coordination etc., and concentrate only on connecting to S3.
The final step is to use the bucket returned from the factory to write the string returned by the serializer:
it 'saves the serialized object to an S3 bucket' do
bucket_factory = double('bucket factory')
bucket = double('S3 bucket')
allow(bucket_factory).to receive(:get_bucket).and_return(bucket)
input = MyObject.new
serializer = double('serializer')
allow(serializer).to receive(:serialize).and_return('serialized object')
expect(bucket).to receive(:put).with('serialized object')
described_class.new(serializer, bucket_factory).save(input)
end
And that’s the repository tested!
Now, take a second to try and imagine what the code that satisfies these tests looks like. Don’t peek below!
Is this what you had in mind?
class Repository
def initialize(serializer, bucket_factory)
@serializer = serializer
@bucket_factory = bucket_factory
end
def save(obj)
serialized = @serializer.serialize(obj)
bucket = @bucket_factory.get_bucket
bucket.put(serialized)
end
end
Analysis
The resulting code looks laughably simple.
Why did I go to all that trouble with faking, testing etc. just for this, frankly trivial piece of code?
The reason is – those tests aren’t the artefacts, or the result, of my coding process. They are the coding process.
If I hadn’t used them to guide my coding, my code would’ve looked completely different. here are some of the advantages I saw with this process, and its result:
- It encouraged me to separate responsibilities to other objects.
With black-box testing it could be tempting to implement everything inside the same class. (Note: the red-green-refactor cycle of TDD should help me get a similar result, as I would’ve refactored my implementation once it was working.
However, using mocks removed a lot of friction out of refactoring, as I only had to change little bits of test code, and no production code.) - I defined the APIs of the collaborating objects before even implementing them. That means that, by definition, those APIs don’t expose any implementation details. The result is very loosely-coupled code.
(In fact, we went through several different types of serializers, but our tests never changed) - The tests are super fast:
Finished in 0.00524 seconds (files took 0.05876 seconds to load)
- The code and tests are isolated; the only dependency required to run the above code and tests is `RSpec` (testing library)
There is, of course, one very glaring problem:
The tests for my repository class are all passing, but it doesn’t actually persist any objects!
I still need integration tests that verify that the class is doing what it’s meant to do.
However, with extensive unit tests, the slower, more brittle, integration tests can be limited to a narrow set of scenarios.
Conclusion
For a while now, I’ve found that writing tests first helped me define my code’s interfaces more clearly.
Having tests as the first users of the code allowed me to look at it from the outside. That meant that the needs of the code’s users determine how the code is used, not any implementation details of the code itself.
Using mocks to drive the implementation takes this process one step further – I can now define other objects’ interfaces, driven by the user of those objects (which is my code under test).
In the example above, I defined the interfaces of the `Serializer` and `BucketFactory` classes while thinking of the `Repository` class.
The next thing I’d like to think about, is the long-term value of these unit tests:
Now that I have my nice, loosely-coupled design, have those tests served their purpose? Do they provide value anymore?
One thought on “Mock-driven development”