A couple things have been bothering me about python’s
Problem 1: Stubs aren’t Mocks
Here’s a function (that is stupid and dumb because this is an example):
def get_next_page(repo, current_page): return repo.get_page(current_page + 1)
If I want to test this with
unittest.mock, it would look like this:
def test_it_gets_next_page_from_repo(self): repo = Mock() next_page = get_next_page(repo, current_page=1) self.assertEqual(next_page, repo.get_page.return_value) repo.get_page.assert_called_with(2)
What bothers me is that I’m forced to use a mock when what I really want is a stub. What’s the difference? A stub is a test double that provides canned responses to calls. A mock is a test double that can verify what calls are made.
Look at the implementation of
get_next_page. To test this, all I really need is a canned response to
repo.get_page(2). But with
unittest.mock, I can only give a canned response for any call to
repo.get_page. That’s why I need the last line of my test to verify that I called the method with a
2. It’s that last line that bothers me.
If I’m writing tests that explicitly assert that specific calls were made, I prefer those to be verifying commands, not queries. For example, imagine I have some code that looks like this:
# ... article.publish() # ...
with tests like this:
def test_it_publishes_the_article(self): article.publish.assert_called_once_with()
Now the assertion in my test feels right. I’m telling the article to publish, so my test verifies that I sent the publish message to the article. My tests are verifying that I sent a command, I triggered some behavior that’s implemented elsewhere. Feels good. But wait…
Problem 2: Public API conflicts
Here’s the other problem. Imagine I had a typo in my test:
def test_it_publishes_the_article(self): article.publish.assertt_called_once_with()
Notice the extra “t” in “assert”? I hope so, because this test will pass even if
article.publish is never called. Because every method called on a
unittest.mock.Mock instance returns another
The problem here is that python’s mocks have their own public api, but they are supposed to be stand-ins for other objects that themselves have a public api. This causes conflicts. Have you ever tried to mock an object that has a
name attribute? Then you’ve felt this pain (passing
name as a
Mock kwarg doesn’t stub a name attribute like you think it would, instead if names the mock).
Doesn’t autospec fix this problem?
autospec is an annoying bandage over this problem. It doesn’t fit into my normal TDD flow where I use the tests to tease out a collaborator’s public API before actually writing it.
I decided to write my own test double library to fix these problems, and I am very happy with the result. I called it tdubs. See the README for installation and usage instructions. In this post I’m only going to explain the parts that solve the problems I described above.
In tdubs, stubs and mocks are explicit. If you want to give canned responses to queries, use a
Stub. If you want to verify commands, use a
Mock. (you want to do both? rethink your design [though it’s technically possible with a
Stub can provide responses that are specific to the arguments passed in. This lets you create true stubs. In the example above, using tdubs I could have stubbed my repo like this:
repo = Stub('repo') calling(repo.get_page).passing(2).returns(next_page)
and I would not need to verify my call to
repo.get_page, because I would only get my expected next page object if I pass
2 to the method.
With tdubs, there’s no chance of false positives due to typos or API conflicts, because tdubs doubles have no public attributes. For example, you don’t verify commands directly on a tdubs
Mock, you use a verification object:
After hammering out the initial implementation to solve these specific problems, I ended up really liking the way my tests read and the type of TDD flow that tdubs enabled. I’ve been using it for my own projects since then and I think it’s ready to be used by others. So if you’re interested, visit the the readme and try it out. I’d love some feedback.