In previous discussions about mock-free unit testing, I’ve shown techniques that I use to eliminate the hard-to-eliminate test doubles. I’ve skipped the simple techniques that I apply all the time. Time to fix that.
One technique eliminated about 90% of the test doubles in my code. It’s simple. It’s been around for a long time. Odds are you already do it but not frequently enough. It even has a name:
Extract Method.
Let’s start by looking at a common example that is used to motivate unit testing with mocks. I made this one up, but it is similar to many that I’ve seen (made slightly more ugly, like a lot of code that I see in real projects).
def send_build_analysis(build_result, smtp_service): msg = smtp_service.create_email(cc='wholeteam@example.com', from='buildbot@example.com') if build_result.succeeded: msg.subject = "Build %d succeess." % build_result.number body = SUCCESS_MESSAGE_BEGIN else: msg.subject = "Build %d failed: %s" % \ (build_result.number, build_result.failure_summary) msg.to = ';'.join(build_result.committer_email_addresses) body = FAILURE_BEGIN_TEMPLATE % build_result msg.attachments += build_result.create_timings_chart() msg.attachments += build_result.static_code_health_analysis() msg.body = body \ + (BUILD_BODY_INFO_TEMPLATE % build_result) \ + MESSAGE_END smtp_service.send(msg)
How would you test this?
Easy: it’s all set up for dependency injection. Just give it a spy smtp_service
. Run the function and make sure that it called service.send()
. Check that the parameter is the email we expect.
But why coddle the poor design?
The fact that we have to test it with a mock is telling us something.
Or look at it another way: is the name of the function correct? I posit that a more accurate name would be analyze_build_result_and_send_as_email()
. And this makes obvious the number one problem with the code as written.
It has multiple responsibilities.
Let’s fix that.
def send_build_analysis(build_result, smtp_service): smtp_service.send(analyze_build(build_result, smtp_service)) def analyze_build(build_result, smtp_service): msg = smtp_service.create_email(cc='wholeteam@example.com', from='buildbot@example.com') if build_result.succeeded: msg.subject = "Build %d succeess." % build_result.number body = SUCCESS_MESSAGE_BEGIN else: msg.subject = "Build %d failed: %s" % \ (build_result.number, build_result.failure_summary) msg.to = ';'.join(build_result.committer_email_addresses) body = FAILURE_BEGIN_TEMPLATE % build_result msg.attachments += build_result.create_timings_chart() msg.attachments += build_result.static_code_health_analysis() msg.body = body \ + (BUILD_BODY_INFO_TEMPLATE % build_result) \ + MESSAGE_END return msg
And how do I test this? Well, first of all I verify that smtp_service.send()
is already tested somewhere. Given that, I don’t feel a need to test the outer method (send_build_analysis()
) at all. I’m bored before I write one test for it.
This simple refactoring changes a side effecting function with complex logic into a tiny function composition, a side-effecting logic that is write-only, and a pure, side-effect free function. Much easier to test. Easier to make the next refactoring (the big function has feature envy and primitive obsession, both of which could be addressed). Also easier to re-use or to extend later.
These are the kinds of trivial refactorings that I use most of the time to eliminate mocks. Sometimes I need the big guns. But often I just need to introduce a method that should have been there anyway.
Exactly! I did 3 videos on this, simple mocks, and then using them together. In the end, you should be able to handle your need for mocks with simple lambdas.
More Here: http://blog.approvaltests.com/2012/03/testing-dif…
I also think it is worth noting that the functional aspect of the underlying method does a lot to enable testing in general. If you think about it all unit testing is wrapping code into a functional harness.
Deterministic,
All inputs in
No Side Effects
Immutable per scope
All results returned
And when tests are not, that results in a smell
Non Deterministic = Intermittent failing
Not All inputs in = Non automated, or random input (which becomes intermittent failing)
Side Effects = Order you run tests in matters
Immutable per scope = can't run tests in parallel.
All results returned = Doesn't fail when it should
Once this is realized, it becomes obvious that it is easier to wrap a piece of functional code into a functional harness, than a piece of non-functional code.
I'm confused. How do you test #send_build_analysis? So far, from what I can tell, you've simply chosen not to test something. I don't think it's too surprising that if you choose not to test something, then you won't mock its collaborators. 🙂
I don't test it. I test until bored. And I am bored with send_build_analysis() before I even start testing it.
If send_build_analysis starts to get more complicated, then it becomes interesting and I test it. It might end up as a sequence of method calls. Or it might get control flow around a sequence of method calls. In either case, its only "statements" are method calls. At that point I pull out the power tools.
The goal, then is simply to make sure that the right calls happen. This can be done with mocks: it is actually an appropriate use of them. The tests won't be oversensitive, because I'm not trying to test that a method achieves a goal, but instead that, given a particular set of inputs, in executes a particular call sequence. That is much more likely to stay stable through refactorings (assuming decent method decomposition) and through changes to the business domain (unless you are actually trying to change this one business process, in which case this will be the one test that is supposed to fail).
All that said, I still might not test with mocks. For example, this case (and many like it) is a data transformation pipeline. The approach I just mentioned is to model a data pipeline as a sequence of function compositions, and then test it by mocking.
Another approach is to make data transformation into a first-class concept. Then we can test the pipeline class once. For a particular pipeline, we test each step independently and then we assert that the pipeline chains the right steps in order. We don't call the pipeline at all: we just ask what it's state is and make sure that the right parts are hooked up in the right order.
This reification of the data pipeline is a no mock approach. I don't go to it immediately, but I use it as soon as I have two data pipelines or I have language support for data pipelining (e.g., C#'s Task<T>, Twisted's futures, or Haskel's monads).
To put it another way, I split the interesting part from the hard-to-test part. Then I test the interesting part. There is no need (or value) to testing the hard part if I've already tested all the business value.
Before the extract method, business value and dependency are intertwined. Afterwards, they're separate.
I suppose I really dislike your title, as it appears to blame mocks for doing their job: drawing the programmer's attention to unnecessary dependencies. "Easily Avoid Pointless Dependencies", for example, at least highlights the underlying problem.
I think you might be missing something. The multiple uses of smtp_service in the new version is a clue that is not being listened to. It suggests a mix of responsibilities on the object which is no longer visible because you've dropped some of the testing.
A better solution might be for this level to create a mail request that it passes to a Messenger that constructs and sends the concrete email.
I agree, and the "final" result is far from optimal. But I wanted this blog entry to show one single big idea: refactor side effect to return value.
In would probably also apply refactor factory to parameter (or some variant thereof) immediately following. That would certainly make the code better.
And the testing still provides that feedback: in order to test the code, I have to set up a smtp_service object that can create messages. So I've still got a non-injected dependency. Furthermore, that dependency is used to "decide what to do and give me back an object that contains that knowledge (the message)." This makes testing hard (makes me want to reach for a mock with a return value provider). My standard solution is to extract the final information object (Message) to a parameter & calculate them separately.
So I don't think that the reduction in testing is removing design feedback. Adding a test (of any kind) on send_build_analysis() wouldn't let me see any more pain.
Most importantly, the one simple refactoring addressed the first pain, and let me test with one fewer mock. And that's all I wanted to show in this one transformation.
While agreeing on the general idea, I don't agree on one thing:
why don't refactor out the fist line of
send_build_analysis
msg = smtp_service.create_email(cc='wholeteam@example.com',
from='buildbot@example.com')
instead of creating a message and then hydrating it with some logic, I always prefer to have the logic in a pure function that doesn't require services, then having a method create_and_send_mail(subj, body, to, attach) with all the required parameter
in this way you can test the pure function as much as you want easily and then you just need to test the create_and_send_mail with some values and the mock of the service.
The problem with mocks is that we should avoid to test both logic (=algorithm) and iteractions in the same test.
That would be the next refactoring. I just wanted to show the first. I am not claiming the second is perfect. Only that it is better. With one very simple change.