#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