Skip to content

Instantly share code, notes, and snippets.

@z4r
Last active October 11, 2015 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save z4r/3870731 to your computer and use it in GitHub Desktop.
Save z4r/3870731 to your computer and use it in GitHub Desktop.
TDD and Unittest

#TDD and Unittest#

Ispirato al tutorial di Jason Diamond:

##Problema##

10 Ottobre 2012: Iniziare a scrivere il tutorial per Unittest
19 Maggio: Buon Anniversario, Tesoro!
19: Dare l'Advocate a Luis e Cleo

##Primo Test e Prima Soluzione##

# test.py
import unittest
import datetime
from pattern import DatePattern

class DatePatternTestCase(unittest.TestCase):
    def test_match(self):
        p = DatePattern(19, 5, 2012)
        d = datetime.date(2012, 5, 19)
        self.assertTrue(p.matches(d))

if __name__ == '__main__':
    unittest.main()

Per far passare questo test abbiamo due soli obiettivi:

  • Rispettare la signature della classe DatePattern
  • Affermare che tutte le date sono uguali tra loro.
# pattern.py
class DatePattern:
    def __init__(self, day, month, year):
        pass

    def matches(self, date):
        return True

Ricordiamo sempre che devono essere i test a guidarci: cosa può stimolare allora il nostro bel programmatore pigro a scrivere un po' più di codice?

##Secondo Test e Prima Esplosione##

# test.py
...
    def test_match_error(self):
        p = DatePattern(19, 5, 2012)
        d = datetime.date(2012, 5, 20)
        self.assertFalse(p.matches(d))
...

Eccoci davanti alla realtà dei fatti:

il 19 Maggio 2012 deve essere diverso dal 20 Maggio 2012

# pattern.py
import datetime

class DatePattern:
    def __init__(self, day, month, year):
        self.date = datetime.date(year, month, day)

    def matches(self, date):
        return self.date == date

Ok, ora la prima specifica è completata! Cerchiamo di dare un volto alla wildcard! ##Wild Cards##

# test.py
...
    def test_match_wild_year(self):
        p = DatePattern(19, 5)
        d = datetime.date(2012, 5, 19)
        self.assertTrue(p.matches(d))

    def test_match_wild_month(self):
        p = DatePattern(19)
        d = datetime.date(2012, 5, 19)
        self.assertTrue(p.matches(d))
...

Uhm, la signature è cambiata e la logica di match è meno built-in.

# pattern.py
class DatePattern:
    def __init__(self, day, month, year=None):
        self.day   = day
        self.month = month
        self.year  = year

    def matches(self, date):
        return (
            (self.year is not None and self.year == date.year or True) and
            (self.month is not None and self.month == date.month or True) and
            self.day == date.day
        )

Siamo abbastanza soffisfatti, diciamo che forse una ritoccatina al metodo matches viene voglia di dargiela, ma per ora può stare lì!!! I test ora sono tutti corretti e possiamo rilassarci DatePattern 1.0 è pronto per il rilascio. ##La nuova Specifica (a.k.a. Il colpo basso)

Sabato: Spesona alla COOP

Nella aggiungere questa nuova funzionalità dobbiamo fare di tutto per mantenere la retrocompatibilità, e il non modificare i test precedenti ci da buone probabilità che nulla si sia rotto. Aggiungiamo quindi un nuovo test per il giorno della settimana.

# test.py
...
    def test_match_weekday(self):
        p = DatePattern(weekday=5) # 5 is Saturday
        d = datetime.date(2012, 5, 19)
        self.assertTrue(p.matches(d))
...

Ormai abbiamo capito il giochino delle wildcard quindi codice come se piovesse.

# pattern.py
class DatePattern:
    def __init__(self, day=None, month=None, year=None, weekday=None):
        self.day   = day
        self.month = month
        self.year  = year
        self.weekday = weekday

    def matches(self, date):
        return (
            (self.year is not None and self.year == date.year or True) and
            (self.month is not None and self.month == date.month or True) and
            (self.day is not None and self.day == date.day or True) and
            (self.weekday is not None and self.weekday == date.weekday() or True)
        )

BOOM!!! Con il codice così scritto non riusciamo nemmeno a capire come mai un vecchio test si sia rotto. It's refactoring time!

# pattern.py
class DatePattern:
    def __init__(self, day=None, month=None, year=None, weekday=None):
        self.day   = day
        self.month = month
        self.year  = year
        self.weekday = weekday

    def matches(self, date):
        return (
            self.year_matches(date) and
            self.month_matches(date) and
            self.day_matches(date) and
            self.weekday_matches(date)
        )
    
    def year_matches(self, date):
        if not self.year: return True
        return self.year == date.year
    
    def month_matches(self, date):
        if not self.month: return True
        return self.month == date.month
    
    def day_matches(self, date):
        if not self.day: return True
        return self.day == date.day
    
    def weekday_matches(self, date):
        if not self.weekday: return True
        return self.weekday == date.weekday()

I test ora sono tutti corretti e possiamo rilassarci DatePattern 1.1 è pronto per il rilascio. ##La nuova Specifica (a.k.a. $!%%##%$!!)

il terzo mercoledì del mese: Pulizia della strada
l'ultimo venerdì del mese: Pizza e Cinema
l'ultimo giorno del mese: controllare l'estratto conto

Con queste due specifiche abbiamo definitivamente capito che l'approccio iniziale non ci basta più e che molto probabilmente anche l'interfaccia ci sta un po' stretta. ###DatePattern 2.0 Abbiamo capito che il match di una data si può tradurre nella composizione dei match di singoli pezzi con più o meno logica; assomiglia molto al pattern Composite:

Il Composito è fondamentalmente un oggetto che contiene altri oggetti, in cui sia l'oggetto Composito sia i suoi oggetti Contenuti implementano la stessa interfaccia. Utilizzando l'interfaccia del Composito dovrebbero essere richiamati i metodi stessi di tutti gli oggetti contenuti senza forzare il client esterno a farlo in modo esplicito.

Sì sì, ci piace. partiamo dal riscrivere test e classe con questo nuovo pattern

# test.py
import unittest
import datetime
from pattern import *

class DatePatternTestCase(unittest.TestCase):
    def setUp(self):
        self.pattern = DatePattern()
        self.date = datetime.date(2012, 5, 19)

    def test_match(self):
        self.pattern.add(DayPattern(19))
        self.pattern.add(MonthPattern(5))
        self.pattern.add(YearPattern(2012))
        self.assertTrue(self.pattern.matches(self.date))

    def test_match_error(self):
        self.pattern.add(DayPattern(19))
        self.pattern.add(MonthPattern(5))
        self.pattern.add(YearPattern(2012))
        d = datetime.date(2012, 5, 20)
        self.assertFalse(self.pattern.matches(d))

    def test_match_wild_year(self):
        self.pattern.add(DayPattern(19))
        self.pattern.add(MonthPattern(5))
        self.assertTrue(self.pattern.matches(self.date))

    def test_match_wild_month(self):
        self.pattern.add(DayPattern(19))
        self.assertTrue(self.pattern.matches(self.date))

    def test_match_weekday(self):
        self.pattern.add(WeekdayPattern(5))
        self.assertTrue(self.pattern.matches(self.date))

if __name__ == '__main__':
    unittest.main()

Già che ci trovavamo, ho sistemato anche un po' di linee ripetute usando il setUp degli UnitTest. Ed ecco il codice rifattorizzato:

class DatePattern:
    def __init__(self):
        self.patterns = []

    def add(self, pattern):
        self.patterns.append(pattern)

    def matches(self, date):
        for pattern in self.patterns:
            if not pattern.matches(date):
                return False
        return True

class YearPattern:
    def __init__(self, year):
        self.year = year

    def matches(self, date):
        return self.year == date.year

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

Abbiamo aggiunto 4 nuove classi, e nonostante la pigrizia ormai ci abbia assaliti in seguito ai festeggiamenti post rifattorizzazione dobbiamo fare gli UnitTest di queste nuove arrivate. Assomiglieranno molto a questo.

# test.py
...
class YearPatternTests(unittest.TestCase):
    def setUp(self):
        self.date = datetime.date(2012, 5, 19)

    def testYearMatches(self):
        p = YearPattern(2012)
        self.assertTrue(p.matches(self.date))

    def testYearDoesNotMatch(self):
        p = YearPattern(2003)
        self.assertFalse(p.matches(self.date))

A questo punto aggiungere regole significa combinarne di vecchie:

Venerdì 17: meglio restare a casa

O scrivere il pattern necessario secondo il Principio di singola responsabilità (reso popolare in Agile Software Development, Principles, Patterns, and Practices da Robert C. Martin a.k.a. Uncle Bob).

Per i più maliziosi il codice finale si trova in appendice :) Ora abbiamo già 7 mattoncini e possiamo sbizzarrirci nel creare sempre più regole: nessuna specifica sarà più un problema per noi.

Compito per casa:

Martedì e Venerdì: Non si parte e non si da principio all'arte
import datetime
class DatePattern(object):
def __init__(self):
self.patterns = []
def add(self, pattern):
self.patterns.append(pattern)
def matches(self, date):
for pattern in self.patterns:
if not pattern.matches(date):
return False
return True
class YearPattern(object):
def __init__(self, year):
self.year = year
def matches(self, date):
return self.year == date.year
class MonthPattern(object):
def __init__(self, month):
self.month = month
def matches(self, date):
return self.month == date.month
class DayPattern(object):
def __init__(self, day):
self.day = day
def matches(self, date):
return self.day == date.day
class WeekdayPattern(object):
def __init__(self, weekday):
self.weekday = weekday
def matches(self, date):
return self.weekday == date.weekday()
class LastWeekdayPattern(WeekdayPattern):
def matches(self, date):
return (
super(LastWeekdayPattern, self).matches(date) and
(date + datetime.timedelta(7)).month != date.month
)
class NthWeekdayPattern(WeekdayPattern):
def __init__(self, n, weekday):
super(NthWeekdayPattern, self).__init__(weekday)
self.n = n
def matches(self, date):
return (
super(NthWeekdayPattern, self).matches(date) and
self.n == self.get_weekday_number(date)
)
@staticmethod
def get_weekday_number(date):
n = 1
while True:
previousDate = date - datetime.timedelta(7 * n)
if previousDate.month == date.month:
n += 1
else:
return n
class LastDayInMonthPattern(object):
def matches(self, date):
return (date + datetime.timedelta(1)).month != date.month
import unittest
import datetime
from pattern import *
class DatePatternTestCase(unittest.TestCase):
def setUp(self):
self.pattern = DatePattern()
self.date = datetime.date(2012, 5, 19)
def test_match(self):
self.pattern.add(DayPattern(19))
self.pattern.add(MonthPattern(5))
self.pattern.add(YearPattern(2012))
self.assertTrue(self.pattern.matches(self.date))
def test_match_error(self):
self.pattern.add(DayPattern(19))
self.pattern.add(MonthPattern(5))
self.pattern.add(YearPattern(2012))
d = datetime.date(2012, 5, 20)
self.assertFalse(self.pattern.matches(d))
def test_match_wild_year(self):
self.pattern.add(DayPattern(19))
self.pattern.add(MonthPattern(5))
self.assertTrue(self.pattern.matches(self.date))
def test_match_wild_month(self):
self.pattern.add(DayPattern(19))
self.assertTrue(self.pattern.matches(self.date))
def test_match_weekday(self):
self.pattern.add(WeekdayPattern(5))
self.assertTrue(self.pattern.matches(self.date))
class YearPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testYearMatches(self):
p = YearPattern(2012)
self.assertTrue(p.matches(self.date))
def testYearDoesNotMatch(self):
p = YearPattern(2003)
self.assertFalse(p.matches(self.date))
class MonthPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testMonthMatches(self):
p = MonthPattern(5)
self.assertTrue(p.matches(self.date))
def testMonthDoesNotMatch(self):
p = MonthPattern(6)
self.assertFalse(p.matches(self.date))
class DayPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testDayMatches(self):
p = DayPattern(19)
self.assertTrue(p.matches(self.date))
def testDayDoesNotMatch(self):
p = DayPattern(20)
self.assertFalse(p.matches(self.date))
class WeekdayPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testWeekdayMatches(self):
p = WeekdayPattern(5)
self.assertTrue(p.matches(self.date))
def testWeekdayDoesNotMatch(self):
p = WeekdayPattern(6)
self.assertFalse(p.matches(self.date))
class LastWeekdayPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testLastWeekdayMatches(self):
p = LastWeekdayPattern(5)
d = datetime.date(2012, 5, 26)
self.assertTrue(p.matches(d))
def testLastWeekdayDoesNotMatch(self):
p = LastWeekdayPattern(5)
self.assertFalse(p.matches(self.date))
class NthWeekdayPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testNthWeekdayMatches(self):
p = NthWeekdayPattern(3, 5)
self.assertTrue(p.matches(self.date))
def testNthWeekdayDoesNotMatch(self):
p = NthWeekdayPattern(1, 5)
self.assertFalse(p.matches(self.date))
class LastDayInMonthPatternTests(unittest.TestCase):
def setUp(self):
self.date = datetime.date(2012, 5, 19)
def testLastDayInMonthMatches(self):
p = LastDayInMonthPattern()
d = datetime.date(2012, 5, 31)
self.assertTrue(p.matches(d))
def testLastDayInMonthDoesNotMatch(self):
p = LastDayInMonthPattern()
self.assertFalse(p.matches(self.date))
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment