One of the first Twisted mail issue tickets I tackled involved what seemed to
be the simple matter of adding missing unit tests but led to some refactoring
of Twisted code. The ticket
highlights the missing unit tests for the startMessage and exists functions
of both the AbstractMaildirDomain and DomainQueuer classes.
AbstractMaildirDomain is a base class which is meant to be subclassed to
create mail domains where the emails are stored in the Maildir format. Most of
the functions it provides are placeholders that need to be overridden in
subclasses. However, it does provide implementations for two functions,
exists and startMessage. exists checks whether a user exists in the
domain or an alias of it and, if so, returns a callable which returns a
MaildirMessage to store a message for the user. Otherwise, it raises an
SMTPBadRcpt exception. startMessage returns a MaildirMessage
which stores a message for the user.
The existing unit tests for AbstractMaildirDomain were minimal, checking just
that it fully implemented the IAliasableDomain interface.
Additional test cases were needed to verify that:
startMessagereturns aMaildirMessage- for a valid user,
existsreturns a callable which returns aMaildirMessageand that the callable returns distinct messages when called multiple times - for an invalid user,
existsraisesSMTPBadRcpt
AbstractMaildirDomain could not be used directly in testing exists and
startMessage because both call a function which has only a placeholder in the
base class. So for testing purposes, I created a subclass of
AbstractMaildirDomain, TestMaildirDomain, which overrides the placeholder
functions.
Since each test would need a TestMaildirDomain to exercise, I wrote a setUp
function which runs prior to the test and creates a TestMaildirDomain as well
as a temporary Maildir directory for it to use. I also added a tearDown
function which runs after each test to remove the temporary directory.
1 2 3 4 5 6 7 8 9 10 | |
The tests for startMessage and exists turned out to be pretty
straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Like AbstractMaildirDomain, DomainQueuer acts as a domain, but instead of
saving emails for users, it puts messages in a queue for relaying. Its
exists and startMessage functions are meant to be used in
the same way as those of AbstractMaildirDomain. It seemed like it would be
an easy matter to adapt the unit tests for AbstractMaildirDomain to work for
DomainQueuer.
It turned out, however, that the test for exists on DomainQueuer failed
because a function was being called on a variable set to None. Something
clearly hadn’t been properly configured for the test.
Here’s the DomainQueuer code being exercised in the test:
1 2 3 4 5 6 7 8 9 10 11 | |
exists calls willRelay with the protocol which was passed into it as part
of the user argument. willRelay tries to call getPeer on the
transport instance variable of the protocol. But, the minimal User
object passed to exists, which worked for the AbstractMaildirDomain test,
is not sufficient for the DomainQueuer test. willRelay needs the protocol
to determine whether it should relay the message.
I had two thoughts about how to get around this problem, but I wasn’t sure
either of them was satisfactory. One option would be to provide the User
object with a fake protocol and fake transport so that willRelay could be
configured to return the desired value. That seemed to be a very roundabout
way to solve the problem of getting willRelay to return a specific boolean
value.
A more direct way to get willRelay to return the desired value would be to
monkeypatch it. That is, as part of the unit test, to replace the
DomainQueuer.willRelay function with one that returns the desired value. The
problem with monkeypatching is that, even though willRelay is part of the
public interface of DomainQueuer, it could change in the future. For
example, it could received additional optional arguments. Then, the unit
test which used the monkeypatched version might not reflect the behavior of
the real version of willRelay.
I got some great advice about how to approach this problem and unit testing in
general on the Twisted developers IRC channel. The idea behind unit testing is
to test each unit of code independently. Here we can consider exists and
willRelay to be two different units. But the way willRelay is implemented
means that it is difficult to test exists independent of it. I was advised
that sometimes the best thing to do when adding unit tests is to refactor the
code itself. So, I attempted to do that without changing the public interface
of DomainQueuer.
I introduced a base class, AbstractRelayRules, whose purpose is to
encapsulate the rules for determining whether a message should be relayed.
Then, I defined a subclass, DomainQueuerRelayRules, which contains the
default rules for the DomainQueuer class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
In DomainQueuer, I changed the __init__ function to take a new optional
parameter specifying the rules to be used to determine whether to relay. When
relayRules is not provided, the default DomainQueuerRelayRules is used. I
also changed the willRelay function to ask the relayRules whether to relay
instead of determining that itself. Existing code which creates a
DomainQueuer without providing the extra argument works in the exact same way
as the old code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Finally, I created another subclass of AbstractRelayRules to be used for
test purposes. TestRelayRules can be configured with a boolean value which
determines whether relaying is allowed or not.
1 2 3 4 5 6 7 | |
Now, the unit tests for DomainQueuer.exists using TestRelayRules are quite
simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Refactoring to decouple the rules for relaying messages from the
DomainQueuer certainly made the unit testing code much cleaner. As a general
matter, difficulty in writing unit tests may highlight dependencies in the code
which should be refactored.