Testowanie
luty 23rd, 2007 at 11:29 pm (scheme, testowanie)
Dzisiaj rozszerzymy odrobin? tematyk? wyk??adu i zajmiemy si? pisaniem test??w dla kodu, kt??ry tworzymy. W wi?kszo??ci ?wicze??, gdzie za zadanie mamy stworzy? funkcj? wykonuj?c? jakie?? konkretne przekszta??cenie autorzy podaj? przyk??ad jej u??ycia - przekazane argumenty i oczekiwany wynik. My zajmiemy si? przekszta??ceniem tych przyk??ad??w w wykonywalne specyfikacje, kt??re automatycznie b?d? sprawdza? poprawno??? naszych procedur.
Uwaga techniczna
Do uruchomienia przyk??ad??w znajduj?cych si? w tym wpisie b?dziesz potrzebowa?? interpretera mzscheme i rozszerzenia standardu srfi-78. Kod umie??? w pliku rational.scm, w tym samym katalogu co plik check.scm, a na jego pocz?tku zamie??? instrukcje wczytuj?ce niezb?dne do testowania biblioteki:
(require (lib “23.ss” “srfi”) (lib “42.ss” “srfi”)) (load “check.scm”)Program uruchamiaj poleceniem:
mzscheme -r rational.scm
1. Specyfikacja (nie)formalna
Na wyk??adzie okre??lono interfejs liczb wymiernych poprzez funkcje
make-rat, numer i denom,
za?? oczekiwane ich dzia??anie zosta??o streszczone w poni??szych
dw??ch wyra??eniach:
(numer (make-rat n d)) ; => n
(denom (make-rat n d)) ; => d
M??wi?c po ludzku: dla dowolnych liczb ca??kowitych n
i d, licznik (ang. numerator) u??amka
utworzonego przy pomocy wywo??ania procedury
(make-rat n d) ma by? r??wny n, za??
mianownik (ang. denominator) ma by? r??wny
d.
Jako, ??e wyk??ad skupia si? g????wnie na wykorzystaniu tak uzyskanego
interfejsu ja chcia??bym spojrze? na rzecz od strony
implementatora. Wcielimy si? teraz w rol? George’a, kt??ry ma
procedury make-rat, numer i
denom napisa?. Jeszcze raz przyjrzyjmy si? za??o??eniom
jakie nasza implementacja ma spe??nia?:
-
procedura
make-ratpowinna przyjmowa? dwa liczbowe argumenty i zwraca? obiekt reprezentuj?cy u??amek -
procedura
numerpowinna przyjmowa? jeden argument b?d?cy u??amkiem utworzonym przezmake-rati zwraca? jego licznik -
procedura
denompowinna przyjmowa? jeden argument b?d?cy u??amkiem utworzonym przezmake-rati zwraca? jego mianownik
Maj?c tak dok??adnie sprecyzowane wymagania mo??na ju?? usi???? do implementacji. Ja proponuj? jednak chwil? si? z tym wstrzyma? i zastanowi? si? nad sposobem w jaki mo??emy podej??? do, mo??e nie udowodnienia, ale przetestowania poprawno??ci naszego przysz??ego rozwi?zania. Maj?c pod r?k? zestaw prostych test??w obejmuj?cych gam? mo??liwych argument??w, podejdziemy do implementacji o wiele pewniej. W miar? jak nasz kod stopniowo b?dzie przechodzi?? kolejne testy b?dziemy pewni, ??e kierunek w kt??rym zmierzamy jest w??a??ciwy. Kompletno??? za?? poszczeg??lnych test??w pomo??e nam sprosta? wymaganiom ustalonej wcze??niej specyfikacji.
2. Pierwsze testy
Do zakodowania test??w u??yjemy oczywi??cie samego j?zyka Scheme, a dok??adniej skorzystamy z prostej biblioteki do testowania opisanej w “SRFI-78: Lightweight testing“. Wci??? powtarza si? tutaj s??owo “testowanie”, ja jednak wol? my??le? o tych kawa??kach kodu jako o specyfikacji zapisanej w taki spos??b, ??e mo??na j? wykona?. Podobnie jak w przedmowie do Wizard Booka uj?li to autorzy, “mo??liwo??? ich wykonania przez maszyny powinna by? traktowana jako co?? dodatkowego”.
Sk??adnia test??w zdefiniowanych w SRFI-78 jest bardzo prosta.
W praktyce wystarczy znajomo??? sk??adni makra check.
Jest ona nast?puj?ca:
(check wyra??enie-testowane => warto???-oczekiwana)Podczas testowania wyra??enie jest obliczane i jego warto??? jest por??wnywana z warto??ci? oczekiwan?. Je??eli obie si? r????ni? wy??wietlany jest b???d. To wszystko. O tym jak wykorzysta? ten mechanizm w praktyce - poni??ej.
Zapis specyfikacji z punktu pierwszego wygl?da nast?puj?co:
(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y)
Powy??szy kod jest zapisem naszej specyfikacji z jednym ma??ym wyj?tkiem - brakuje tutaj okre??lenia “dla dowolnego” liczbowego argumentu. Spr??bujmy uwzgl?dni? t? kwesti? w naszym te??cie.
(define (check-rat x y)
(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y))
(for-each (lambda (pair-of-args) (apply check-rat pair-of-args))
any-pair-of-arguments)
Pomocnicza procedura check-rat sprawdza poprawno??? wynik??w
generowanych przez procedury make-rat, numer
i denom dla zadanych argument??w x
i y. Argumenty te pochodz? z generatora
any-pair-of-arguments. Niestety zbi??r liczb ca??kowitych
jest niesko??czony, nie mo??emy wi?c wymaga?, by nasz program zako??czy??
testowanie, je??eli zaimplementujemy any-pair-of-arguments
jako niesko??czony zbi??r par (x, y). Zmuszeni jeste??my
ograniczy? testowanie do pewnego sko??czonego zbioru danych testowych.
Wypr??bujmy najpierw prosty dodatni u??amek 1/2:
(define any-pair-of-arguments ‘((1 2)))
Uruchamiamy test poprzez zwyk??e wywo??anie naszego skryptu - modu??
srfi-78 i jego makro check zajmuj? si? reszt?:
mzscheme -r rational.scm
Otrzymujemy b???d:
reference to undefined identifier: make-rat
Rzeczywi??cie, nie zdefiniowali??my jeszcze ??adnej z wymaganych procedur.
Zr??bmy to teraz, troch? jednak oszukuj?c, bo nasze procedury b?d?
zawsze zwraca??y warto??? prawda (#t):
(define (make-rat x y) #t)
(define (numer rational) #t)
(define (denom rational) #t)
Zobaczmy jak zachowa si? nasz test:
(numer rational) => #t ; *** failed *** ; expected result: 1 (denom rational) => #t ; *** failed *** ; expected result: 2
Kod nie spe??nia specyfikacji i nasz test to wykrywa. Dobrze,
spr??bujmy go naprawi?. Zmieni? definicj? numer
i denom:
(define (numer rational) 1)
(define (denom rational) 2)
Jaki b?dzie wynik test??w?
(numer rational) => 1 ; correct (denom rational) => 2 ; correct
Test uznaje nasze procedury za poprawne, chocia?? my wiemy, ??e nasz kod nie jest poprawny. To wyra??ny znak, ??e nasze testy nie s? wystarczaj?ce. Dodajmy wi?c dwa nowe zestawy testowe:
(define any-pair-of-arguments
‘((1 2) (3 4) (5 6)))
Tym razem nasz kod powoduje spodziewane b???dy:
(numer rational) => 1 ; correct (denom rational) => 2 ; correct (numer rational) => 1 ; *** failed *** ; expected result: 3 (denom rational) => 2 ; *** failed *** ; expected result: 4 (numer rational) => 1 ; *** failed *** ; expected result: 5 (denom rational) => 2 ; *** failed *** ; expected result: 6
??eby przej??? wszystkie te przypadki testowe nie mo??emy ju?? oszukiwa?
i jeste??my zmuszeni zapami?ta? przekazane do make-rat
liczby. Skorzystamy z przedstawionych na wyk??adzie par:
(define (make-rat x y) (cons x y))
(define (numer rational) (car rational))
(define (denom rational) (cdr rational))
Gratulacje, nasz kod przechodzi wszystkie testy:
(numer rational) => 1 ; correct (denom rational) => 2 ; correct (numer rational) => 3 ; correct (denom rational) => 4 ; correct (numer rational) => 5 ; correct (denom rational) => 6 ; correct
Modu?? srfi-78 umo??liwia wypisanie podsumowania wszystkich test??w. Wystarczy doda? na koniec pliku nast?puj?c? linijk?:
(check-report)
3. Skracanie u??amk??w
W tej chwili mamy ju?? dzia??aj?ce procedury do tworzenia u??amk??w
i zwi?zane z nimi automatyczne testy. Zazwyczaj kod i testy
umieszcza si? w oddzielnych plikach, wydzielmy wi?c z pliku
rational.scm wszystko co zwi?zane z testami i umie???my
w pliku rational-test.scm, pami?taj?c o dodaniu
dyrektywy wczytuj?cej definicje z oryginalnego pliku
rational.scm: (load "rational.scm").
Po wykonaniu tych czynno??ci pliki rational.scm
i rational-test.scm powinny wygl?da? nast?puj?co:
;; plik rational.scm
(define (make-rat x y) (cons x y))
(define (numer rational) (car rational))
(define (denom rational) (cdr rational))
;; plik rational-test.scm
(require (lib “23.ss” “srfi”) (lib “42.ss” “srfi”))
(load “check.scm”)
(load “rational.scm”)
(define (check-rat x y)
(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y))
(define any-pair-of-arguments
‘((1 2) (3 4) (5 6)))
(for-each (lambda (pair-of-args) (apply check-rat pair-of-args))
any-pair-of-arguments)
(check-report)
Maj?c ju?? gotow? podstawow? funkcjonalno??? zajmiemy si? jej
stopniowym rozszerzaniem. Zaczniemy od skracania u??amk??w.
M??wi?c dok??adnie, chcemy, by dla u??amka utworzonego przy pomocy
(make-rat 2 4), procedura numer
zwraca??a 1, za?? procedura denom zwraca??a 2
(tym samym skracaj?c 2/4 do 1/2). Jak zwykle zacznijmy od
zdefiniowania nowego testu.
(let ((rational (make-rat 2 4)))
(check (numer rational) => 1)
(check (denom rational) => 2))
Wynik testu:
(numer rational) => 2 ; *** failed *** ; expected result: 1 (denom rational) => 4 ; *** failed *** ; expected result: 2
Czas wr??ci? do kodu i zaimplementowa? skracanie. Gdzie to skracanie
zaimplementujemy nie ma znaczenia - mo??emy skraca? zar??wno przy
tworzeniu (make-rat), jak i przy odczycie (procedury
numer i denom). Ja p??jd? na ??atwizn?
i przepisz? z ksi???ki skracanie przy tworzeniu. ;-)
(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))
Dodajmy jeszcze jeden test dla pewno??ci:
(let ((rational (make-rat 15 25)))
(check (numer rational) => 3)
(check (denom rational) => 5))
Nasz kod wci??? przechodzi wszystkie testy, implementacja jest wi?c rzeczywi??cie poprawna.
W naszych testach mo??na jednak zauwa??y? nieprzyjemne powt??rzenia.
Warto wi?c po??wi?ci? chwil? na uog??lnienie naszych przypadk??w
testowych. Zmie??my odrobin? napisan? wcze??niej p?tl?
for-each tak, by zamiast listy par argument??w
przyjmowa??a listy argument??w i oczekiwanych wynik??w.
(define (check-rat arguments expected-numer expected-denom)
(define rational (apply make-rat arguments))
(check (numer rational) => expected-numer)
(check (denom rational) => expected-denom))
(for-each (lambda (args) (apply check-rat args)) test-cases)
Definicja przypadk??w testowych wygl?da? b?dzie teraz nast?puj?co:
(define test-cases
(list (list ‘(1 2) 1 2)
(list ‘(3 4) 3 4)
(list ‘(5 6) 5 6)))
Dodanie ostatnich dw??ch przypadk??w na skracanie jest trywialne:
(define test-cases
(list (list ‘(1 2) 1 2)
(list ‘(3 4) 3 4)
(list ‘(5 6) 5 6)
;; skracanie licznika z mianownikiem
(list ‘(2 4) 1 2)
(list ‘(15 25) 3 5)))
4. Normalizacja znak??w
Mamy ju?? zdefiniowane i przetestowane procedury dotycz?ce u??amk??w
w takiej formie, jak okre??lono je na wyk??adzie. Teraz zajmiemy si?
ich dalszym rozszerzaniem, jako ??r??d??o natchnienia traktuj?c
?wiczenie 2.1.
Naszym zadaniem jest takie zmodyfikowanie procedury make-rat,
by radzi??a sobie zar??wno z dodatnimi, jak i z ujemnymi argumentami.
Dodatkowo znak ca??ego u??amka musi by? normalizowany - u??amek ujemny
ma mie? ujemny licznik i dodatni mianownik, za?? u??amek dodatni ma
mie? dodatni zar??wno licznik, jak i mianownik. Zapiszmy wszystkie cztery
mo??liwe przypadki w postaci test??w:
;; …
;; normalizacja znakow
(list ‘(1 1) 1 1)
(list ‘(-1 1) -1 1)
(list ‘(1 -1) -1 1)
(list ‘(-1 -1) 1 1)))
Gdy uruchomimy nasze testy przekonamy si?, ??e nasz kod przechodzi
pierwsze dwa z nich, generuje jednak b???d dla dw??ch ostatnich.
Dwa ostatnie przypadki s? wi?c tymi, kt??re musimy naprawi?. Dla
argument??w (1 -1) oczekujemy licznika r??wnego -1
i mianownika r??wnego 1. Dla argument??w (-1 -1)
oczekujemy licznika i mianownika r??wnego 1. ??cz?c te dwa przypadki
otrzymujemy zale??no???: je??eli drugi argument make-rat
jest ujemny, odwr??? znaki obu argument??w. Ten w??a??nie warunek
zapiszemy w kodzie.
(define (make-rat n d)
(if (negative? d)
(make-rat (- n) (- d))
(let ((g (gcd n d)))
(cons (/ n g) (/ d g)))))
Gotowe. Wszystkie testy przechodz? bez b???d??w. Wygl?da wi?c na to, ??e uda??o nam si? sprytnie zaimplementowa? wymagan? funkcjonalno??? nie psuj?c niczego po drodze. Pewno???, ??e niczego nie popsuli??my daj? nam w??a??nie testy - o wiele szybciej i pro??ciej pisa? je w trakcie pisania samego kodu, ni?? p????niej debugowa? b???dy wywo??ane zmianami w wi?kszym systemie. Pisanie test??w jest wi?c rodzajem inwestycji. Pisanie kodu zajmuje nam teraz troch? wi?cej czasu, ale oszcz?dzamy czas (i nerwy) w przysz??o??ci. Do tego, jak widzieli??my przed chwil?, testy pomagaj? przy samym pisaniu programu, u??atwiaj?c zdefiniowanie zachowania jakiego oczekujemy od naszych procedur, a czasami nawet wskazuj?c prostsze sposoby rozwi?zania problem??w.
5. Skrajne przypadki
Nasza implementacja u??amk??w potrafi skraca? licznik z mianownikiem
i normalizowa? ich znak. Brakuje teraz jeszcze tylko jednej rzeczy
- obs??ugi sytuacji wyj?tkowych. Zak??adaj?c poprawne typy argument??w
sytuacja taka w przypadku u??amk??w jest tylko jedna - dzielenie przez
zero. W tej chwili make-rat nie protestuje, gdy jako
mianownik przeka??emy mu zero. Procedura powinna jednak zg??osi?
b???d przy pomocy standardowej procedury error. By
przetestowa? sytuacj? wyj?tkow? skorzystamy z makra
with-handlers,
kt??re zajmuje si? obs??ug? wyj?tk??w.
(check (with-handlers ((exn? (lambda (e) ‘error)))
(make-rat 1 0)) => ‘error)
W powy??szym wyra??eniu ka??dy wyj?tek rzucony przez make-rat
zostanie przechwycony przez makro
with-handlers,
a wyra??enie przyjmie warto??? zwr??con? przez podan? funkcj? (w naszym
przypadku warto??ci? t? b?dzie symbol error). Jako, ??e
spodziewamy si? wyj?tku, piszemy => 'error.
Uruchomienie testu pokazuje, ??e spodziewany wyj?tek nie zosta?? rzucony:
(with-handlers ((exn? (lambda (e) (quote error)))) (make-rat 1 0)) \
=> (1 . 0) ; *** failed ***
; expected result: error
Wracamy do definicji make-rat i dopisujemy przypadek
dla mianownika r??wnego zero:
(define (make-rat n d)
(cond ((negative? d)
(make-rat (- n) (- d)))
((zero? d)
(error “Mianownik nie moze byc zerem.”))
(else (let ((g (gcd n d)))
(cons (/ n g) (/ d g))))))
Gdy uruchomimy test ponownie przekonamy si?, ??e nasze zadanie zosta??o wykonane - wyj?tek jest rzucany. Tym samym skutecznie przetestowali??my sytuacj? wyj?tkow?.
Podsumowanie
Pisanie automatycznych test??w r??wnolegle z kodem ma wiele zalet. Zbyt ma??o jest miejsca na tym blogu, by opisa? je wszystkie, zach?cam zatem do zapoznania si? z tematem na w??asn? rek?. Technik? t? nazwano Test Driven Development (w skr??cie TDD), czyli programowanie sterowane testami. Materia????w po polsku niestety nie ma zbyt wiele, pozwol? wymieni? sobie trzy artyku??y. Ka??dy z nich opisuje testowanie w innym j?zyku programowania, zahacza jednak (mniej lub bardziej) r??wnie?? o sam? technik?.
Powodzenia w testowaniu!