Last week we discussed dependency injection, the idea that you can change the implementation of a function or class that you actually use to perform a specific task at run time. This is a very useful strategy, in part because it makes our code easier to test because we can inject "fake" dependencies that allow themselves to be introspected and otherwise verified. But what's the deal with these "fakes"? What are they? Where do they come from? Do we have to write them ourselves?
There several kinds of "fake" dependencies, but as a group they are often referred to as "test doubles". The following definitions are taken from Martin Fowler's article "Mocks aren't Stubs".
We're not going to talk about all of these (spies and mocks are a little more complicated and vary quite a bit between implementations), but you should know they exist and that they are, at least often, considered to be different from one another.
A dummy object is just a placeholder. Dummy objects generally have no behavior at all and simply serve to satisfy the compiler. For example, we might use a dummy cache object when testing a system with caching disabled.
class Responder:
def __init__(self, cache):
self._cache = cache
def respond(self, useCache):
if useCache:
return self.cache.getResponse()
else:
# generate a real response
pass
class ResponderTest:
def test_respond_with_no_cache(self):
responder = Responder(None)
# test the respond() method
In a statically typed language this would be a bit more complicated, but in a language like Java we might just pass "null" as a kind of dummy object. In other languages we would need a real object, but it wouldn't need to actually do anything.
This is one of the most common test doubles when working with network services and databases because it is undesirable for unit tests to require access to the network or a database server. The classic example of a fake object is one that allows access to a database. Rather than storing its data in a real database, it simply holds onto it and returns it when requested.
class FakeMessageDatabase:
def __init__(self):
self._messages = []
def createMessage(self, to, from, text):
message = Message(to, from, text)
self._messages.append(message)
def getMessagesFrom(self, from):
return [m for m in self._messages if m.from == from]
def getMessagesTo(self, to):
return [m for m in self._messages if m.to == to]
Now we could use this anywhere that requires a real "MessageDatabase". It doesn't actually store messages in a real database, as we would expect in the version of our app we deploy to customers, but it is much less complicated to use and doesn't require a running database instance.
A stub is a little like a fake object, but it generally only returns "canned" responses and doesn't usually "do" anything behind the scenes. To continue the example from above, we could create a "StubMessageDatabase" pretty easily. Note, however, that since it doesn't actually attempt to store any messages, we can't use it in situations where we want to write a new message and then read it back. However, we could use something like this to set, for example, user interface code that just reads messages and prints to the screen in some way.
class StubMessageDatabase:
def createMessage(self, to, from, text):
pass
def getMessagesFrom(self, from):
return [Message('recipient 0', from, 'message 0'),
Message('recipient 1', from, 'message 1')]
def getMessagesTo(self, to):
return [Message(to, 'sender 0', 'message 0'),
Message(to, 'sender 1', 'message 1')]