Never miss one of my articles: Readers of my newsletter get my articles before anyone else. Subscribe here!
Today I read a very interesting article by Kent Beck and Martin Fowler: “Half-done Versus Fake: The Intermediate Result/Mock Tradeoff”. It shows some problems with mocking a class. Then the authors conclude that it’s better to split the functionality and test intermediate results instead of using mocks.
The technique they introduce is very interesting. Still, I was immediately thinking: “I wouldn’t write the code that way in the first place”. Don’t get me wrong, I don’t think their code is bad. And I think their technique is useful. But in this particular case, I would re-structure the code differently, so that testing it with mocks becomes easier and makes more sense.
What's Wrong With Their Approach?
The authors us a very simple example: Copy a set of source directories to a destination directory. Here’s their code:
import subprocess class Backup1: def __init__(self, sources, destination): self.sources = sources self.destination = destination def store(self): for source in self.sources: command = ['cp', '-r', source, self.destination] subprocess.call(command)
They would test it with mocks by mocking
subprocess.call and making sure that all the right copy commands are passed to that method. Then they argue that this causes several different problems, like, you tie your functionality to a particular implementation (You couldn’t use Python’s file system utilities instead of “cp” without breaking the test). And they are right!
A class or a method are never too small to split.
The suggested approach from the article was to “extract the scary bits” and then test the intermediate results:
class Backup2: def __init__(self, sources, destination): self.sources = sources self.destination = destination def store(self): [subprocess.call(command) for command in self.commands()] def commands(self): return [['cp', '-r', source, self.destination] for source in self.sources]
You can then test if
Backupt2.commands produces the correct set of “cp” commands. Unfortunately, this approach has the same problems as the first one, with mocks. You still cannot replace “cp” with Python code without breaking the test.
And there is another problem:
Backup2.commands clearly should be private. It is just a simple utility function, and I see no reason why it should be a part of the interface of the backup class. So you either have to make design compromises to test the functionality, or you cannot test it at all!
Anti Corruption Layer
I see two problems in
Backup1 that are somewhat related: It does two different things (iterate over a list of directories to copy and do the actual, low-level copying, and it’s code is on different levels of abstraction (decide what to process and implement how the processing is done).
A class or a method are never too small to split.
So I would split the code too, but along a different line. I would wrap how the copying is actually done in a class that makes sense for the caller (Please forgive me if I didn’t get the Python code completely right. I don’t really know Python that well…):
import subprocess class FileSystemManipulation: def __init__(self): #... def copyDirectory(source, destination): command = ['cp', '-r', source, destination] subprocess.call(command)
This class serves as an “Anti Corruption Layer” to the messy details about how to copy directories (and potentially other file system manipulation).
Aside: I am not sure if “FileSystemManipulation” is a good name here. It probably is not, because it is too generic. But we can always try to find a better name later, when we know more about the application and what we want to achieve.
I can now write the Backup class in a way that uses the new abstraction:
class Backup3: def __init__(self, sources, destination, fileSystemManipulation): self.sources = sources self.destination = destination self.fileSystemManipulation = fileSystemManipulation def store(self): for source in self.sources: self.fileSystemManipulation.copyDirectory(source, self.destination)
How Can You Test This
The responsibility of Backup3 is now much clearer: It processes a list of source directories, and makes sure each gets copied to a single destination directory. I can test this with mocks in a way that makes sense within the domain of Backup3. And I won’t have to change my tests when I decide to implement the actual copying in a different way.
Aside: I would probably create two tests here: One that makes sure that a list of source directories is processed correctly (i.e. all are passed to
fileSystemManipulation.copyDirectory), and one that makes sure that all calls to
fileSystemManipulation.copyDirectory use the same destination directory.
I would then test FileSystemManipulation with an integration test - i.e. a test that copies a directory and observes the file system if the correct behavior happens. This test can make sure that it operates on minimal data, so it will be reasonably fast. And it should check some preconditions (like, is there enough space left on the device) and ignore the test when they are not satisfied. In JUnit, I would do this with
Assume.assumeThat(...), I have no idea how to do it in Python.
Note that I wouldn’t even have to change the test for FileSystemManipulation if I change the way how the copying is done. Since it is a real integration test (i.e. it tests how ONE of my classes interacts with the outside world), the test will still be valid if we decide to use Pythons file system manipulation instead of “cp”.
Turtles All The Way Down
One potential problem here is that we create abstractions that depend on abstractions that depend on… It’s like “Turtles all the way down”. But we can decide to stop at any point. Ultimately, this is a cost-benefit trade-off: What is the cost of more abstraction compared to the benefit of better separation of concerns.
Anyway, I think it (almost) always makes sense to protect our domain classes / functions from the messy details of the outside world. And that’s exactly what
When a class is hard to test, consider splitting it. I absolutely agree on that with Kent Beck and Martin Fowler. But I would do the splitting differently:
First, look at all the different things the code does. Then think about whether the code works on different levels of abstraction. After that, in our example, we found a line where to split the code: Between the different levels of abstraction. This also solved the problems with the function having two different responsibilities.
Also, always protect your domain code from the messy details of the outside world by creating an anti corruption layer. Don’t use “new Date()”, use “wallclock.now()”. Don’t call a rest service to authenticate users directly from your domain code, create a “UserAuthenticator” that encapsulates the call…
Are you interested in managing IT and software projects or teams? Are you in Europe in Autumn 2016? We are preparing a great conference for you: Advance IT Conference
You might be also interested in:
- Simple Design passes its Tests: How software design and testing go hand in hand.
- Cheap plastic drills: Most people think construction workers should have great tools. A lot of people think paying more than 1000 Euros for an office chair is a waste of money, even for a software developer who uses it 8 hours a day. Good tools are expensive.
- REPL Driven Development and Testing in Clojure: Some ideas how you can use a REPL to drive your design and create regression tests.