More Test-Driven Development in Pythonby Jason Diamond
In the first article in this series, Test-Driven Development with Python, Part 1, I started to build an event tracking application using the principle of test-driven development. Before writing any new code or making changes to any existing code, I wrote a failing test first. When the new test started passing, I stopped and moved on. It's a simple technique, but requires discipline in order to apply it successfully.
If you haven't read part one yet, I suggest you do so before continuing on in this article. That will familiarize you with the problem I'm trying to solve and bring you up to speed on my current predicament.
A New Design Emerges
As indicated in the previous article, I'm starting to think that the single
DatePattern class I have so far is responsible for too much. It's
only 30 lines long at this point, but I don't mean its length when I say "too
much." What I mean is, conceptually, it does four different things; I even
started adding a fifth before I stopped myself.
What are its four different responsibilities? It has to determine if the year matches. That's one. The month is another, as are the day and the weekday, too. Can I break those responsibilities into four different classes? I'm sure I can, but should I?
Not only that, but for each component of the pattern, I have to check to see if it's a wild card and act appropriately. Maybe I need yet another class to encapsulate that logic.
I can't answer these questions by just thinking about them, so I will explore by writing a few tests:
class NewTests(unittest.TestCase): def testYearMatches(self): yp = YearPattern(2004) d = datetime.date(2004, 9, 29) self.failUnless(yp.matches(d))
Note that I've put this new test case in a new fixture. If this turns out to
be a dead end, I can easily delete this fixture and continue onward with the
old. If this ends up being a good idea, I can just as easily delete the old
fixture (and the code it exercised, the
This new test case, of course, fails. Here's the code required to make it pass:
class YearPattern: def __init__(self, year): pass def matches(self, date): return True
Boring. How about another test?
def testYearDoesNotMatch(self): yp = YearPattern(2003) d = datetime.date(2004, 9, 29) self.failIf(yp.matches(d))
Now to make the new test pass without breaking the old one:
class YearPattern: def __init__(self, year): self.year = year def matches(self, date): return self.year == date.year
What's the point? Before I can show you that, I need to write a few more tests:
def testMonthMatches(self): mp = MonthPattern(9) d = datetime.date(2004, 9, 29) self.failUnless(mp.matches(d)) def testMonthDoesNotMatch(self): mp = MonthPattern(8) d = datetime.date(2004, 9, 29) self.failIf(mp.matches(d)) def testDayMatches(self): dp = DayPattern(29) d = datetime.date(2004, 9, 29) self.failUnless(dp.matches(d)) def testDayDoesNotMatch(self): dp = DayPattern(28) d = datetime.date(2004, 9, 29) self.failIf(dp.matches(d)) def testWeekdayMatches(self): wp = WeekdayPattern(2) # Wednesday d = datetime.date(2004, 9, 29) self.failUnless(wp.matches(d)) def testWeekdayDoesNotMatch(self): wp = WeekdayPattern(1) # Tuesday d = datetime.date(2004, 9, 29) self.failIf(wp.matches(d))
Here's the code to make these tests pass:
class MonthPattern: def __init__(self, month): self.month = month def matches(self, date): return self.month == date.month class DayPattern: def __init__(self, day): self.day = day def matches(self, date): return self.day == date.day class WeekdayPattern: def __init__(self, weekday): self.weekday = weekday def matches(self, date): return self.weekday == date.weekday()
Is it obvious where I'm going with this yet? If not, this test should make it clear:
def testCompositeMatches(self): cp = CompositePattern() cp.add(YearPattern(2004)) cp.add(MonthPattern(9)) cp.add(DayPattern(29)) d = datetime.date(2004, 9, 29) self.failUnless(cp.matches(d))
What I've stumbled across here is an instance of the Composite Pattern. (Interestingly, this is the same name as my class--that wasn't intentional, I promise.)