Python DevCenter
oreilly.comSafari Books Online.Conferences.

advertisement


More Test-Driven Development in Python

by Jason Diamond
02/03/2005

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))

red

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 DatePattern class).

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

green

Boring. How about another test?

def testYearDoesNotMatch(self):
    yp = YearPattern(2003)
    d = datetime.date(2004, 9, 29)
    self.failIf(yp.matches(d))

red

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

green

Perfect.

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))

red

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()

green

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))

red

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.)

Pages: 1, 2, 3

Next Pagearrow





Sponsored by: