W ostatnim, krótkim wpisie pokazałem jak szybko skonfigurować minimalistyczne środowisko do testowania JavaScriptowego kodu. Przygotowałem tam wszystko co jest potrzebne do pisania podstawowych testów jednostkowych.
Dziś pokażę jak, wykorzystując Moche i Chai, napisać kilka takich podstawowych testów. Zastosuje bardzo modną niegdyś metodologię TDD, czyli Test Driven Development. Już jakiś czas temu pisałem na łamach bloga o tej metodologii, więc dziś nie będę się już rozwodził na ten temat. W skrócie, metodologia ta cechuje się tym, że programy zaczynamy pisać od testów.
Kod, który chcę dziś stworzyć ma służyć do sprawdzania, czy podana liczba jest liczbą pierwszą. Jeżeli faktycznie będzie to liczba pierwsza, zwrócona zostanie wartość true w przeciwnym wypadku otrzymam wartość równą false.
Stan projektu wygląda dokładnie tak samo jak wyglądał na końcu poprzedniego wpisu. W głównym katalogu projektowym znajduje się kolejny katalog: test, który z kolei zawiera plik test.spec.js. Zawartość pliku wygląda tak:
1 2 3 4 5 6 7 |
import { expect } from 'chai'; describe("test test", () => { it("should pass this test test", () => { expect(true).to.eql(true) }) }) |
Tak napisany test, mogę z łatwością uruchomić z konsoli, wywołując zdefiniowany w package.json skrypt npm’owy. Służy do tego taka oto komenda:
1 |
npm test |
Czas dodać do tego trochę kontesktu. W głównym katalogu projektowym tworzę nowy katalog o nazwie src. Wewnątrz nowego katalogu dodaję nowy plik: index.js. Póki co wpisuję w niego minimalistyczną treść, w końcu mam zacząć programowanie od pisania testów. Tak wygląda pierwsza wersja pliku index.js:
1 2 3 |
export class PrimeChecker { constructor(){}; } |
Jest to pusta klasa o nazwie PrimeChecker, którą eksportuję poza moduł. Jak widać używam składni ES6, ponieważ jestem nowoczesny 🙂 . Warto zaznaczyć, że ten kod nie uruchomi się, ponieważ domyślnie Node, nie wspiera takiej składni. Na szczęście dla mnie nie ma to znaczenia. Tak skonfigurowałem moje testy, że w kontekście Mocha, testowany kod będzie zrozumiały. Jeżeli poprawnie napiszę testy i będą one przechodzić, to nie muszę włączać tego kodu, będę wiedział, że działa 🙂 .
Pozostaje kwestia poprawnego napisania testów 🙂 Aby to zrobić muszę najpierw dobrze zrozumieć wymagania kodu. Gdy zrozumiem do czego ma służyć tworzony kod, będę w stanie dobrze rozplanować strategię testów. Uwierzcie mi, to wszystko może wydawać się oczywiste, ale w prawdziwym życiu często takie proste sprawy potrafią się szybko skomplikować.
Chcę aby moja klasa PrimeChecker, zawierała metodę, przyjmującą pewną wartość jako argument. Jeżeli wartość ta będzie liczbą pierwszą, metoda zwróci true, w przeciwnym wypadku metoda zwróci false. Jasna sprawa. Jeżeli jako argument przekazana zostanie wartość nie będąca liczbą, program zgłosi błąd. To samo stanie się, jeśli nie zostanie przekazana żadna wartość.
Ok, zadanie wygląda na dość proste. Nadeszła pora na zaplanowanie testów. Sztuka polega na tym, aby jak najmniejszą ilością testów, sprawdzić wszystkie możliwe zachowania programu. Dla takiego małego fragmentu kodu, jeden test więcej czy mniej może wydawać się mieć małe znaczenie, ale należy pamiętać, że testy te będą często uruchamiane regresyjnie. Każde uruchomienie zajmie trochę czasu. Czas ten szybko zacznie się zwiększać, więc nie można tracić go na zbędne testy. Dlatego chcemy aby nasze testy działały jak najbardziej optymalnie.
Kolejnym krokiem jest rozplanowanie testów. Ponieważ wiem już jakie mam wymagania, ten krok nie powinien sprawić żadnych problemów. Najlepszą strategią jest sprawdzenie pozytywnego zachowania programu następnie negatywnego zachowania a na koniec sprawdzenie wyjątków. Co to oznacza? Mniej więcej to, że muszę sprawdzić, że program zachowa się poprawnie gdy otrzyma liczbę pierwszą (pozytywne zachowanie), gdy otrzyma liczbę, która ma więcej dzielników niż dwa (negatywne zachowanie), oraz gdy otrzyma zupełnie nie poprawne dane (wyjątki).
Do tego dopiszę też test, który sprawdzi zachowanie programu, gdy przekazana zostanie liczba jeden. Robię to dlatego, że liczba jeden jest dość wyjątkowa i chcę sprawdzić czy algorytm to uwzględnia.
Oto lista testów, które chcę napisać:
- przekazanie jedynki, zwróci false
- przekazanie dowolnej innej liczby pierwszej zwróci true
- przekazanie dowolnej liczby o więcej niż dwóch dzielnikach zwróci false
- przekazanie wartości nie będącej liczbą zgłosi błąd
- Nie przekazanie wartości zgłosi błąd
No to lecimy. Składnia Mocha i Chai nie jest zbyt skomplikowana i napisanie takich testów nie powinno sprawić większych problemów. Oto zawartość pliku test.spec.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { expect } from 'chai'; import { PrimeChecker } from '../src/index.js'; describe("PrimeChecker class under test", () => { let checker; beforeEach(() => { checker = new PrimeChecker(); }); it("should pass this test test", () =>{ expect(true).to.eql(true) }) it("should return false, when passed a 1", () =>{ expect(checker.check(1)).to.eql(false); }) it("should return true, when passed a 2", () =>{ expect(checker.check(2)).to.eql(true); }) it("should return false, when passed a 8", () =>{ expect(checker.check(8)).to.eql(false); }) it('should throw an error, when called without a value', () => { var call = () => {checker.check();} expect(call).to.throw(Error,'Invalid argument'); }) it('should throw an error, when passed a value that is not a number', () => { var call = () => {checker.check("piespies");} expect(call).to.throw(Error,'Invalid argument'); }) }) |
Na samym początku importuję klasę z pliku index.js, w końcu muszę mieć co testować 🙂 . Każdy test będzie korzystał z instancji tej klasy, więc tworzę takową wewnątrz bloku beforeEach. Kod ten wykona się przed każdym testem, które opisane zostały w blokach it.
Całość otoczone jest blokiem describe, który definiuje zestaw testów powiązanych tematycznie, zwany także jako test suite.
Do asercji używam Chai’owego expect. Większość kodu powinna być oczywista. Warto zwrócić uwagę na to jak sprawdzam występowanie wyjątków. W takim wypadku do expect przekazuję funkcję anonimową, która w sobie wywołuje kod rzucający błędem. Dla czytelności anonimowe funkcję przypisuje do zmiennych call. Potem wystarczy, użyć metody to.throw(), która jako argument przyjmuej obiekt błędu oraz wiadomość jaką zgłasza. Jeżeli kod w funkcji anonimowej wywoła taki błąd, test przechodzi 🙂 .
Jeżeli teraz uruchomię polecenie npm test, przejdzie tylko jeden test, pierwszy, który był tu od początku. Mógłbym go usunąć, ale wole zawszę mieć taki testowy test gdzieś w swoich suitach. W wypadku gdyby posypało się środowisko, dzięki temu testowi mogę szybko to zauważyć, i nie będę zastanawiał się dlaczego poprawny kod nagle oblewa wszystkie testy 🙂 .
W tym wypadku jednak reszta testów nie przechodzi, ponieważ kodu jeszcze nie ma. Zgodnie z założeniami TDD, będę dopisywał tylko tyle kodu aby każdy kolejny test przechodził. W momencie gdy wszystkie testy zaczną przechodzić, powinienem mieć już w pełni działający kod.
No to lecimy, najpierw muszę sprawić aby przekazanie jedynki zwracało false. Metodę sprawdzającą liczby w klasie PrimeChecker nazwałem w testach check, tego się będę trzymał. Oto pierwsza iteracja zmian w pliku index.js:
1 2 3 4 5 6 7 8 9 |
export class PrimeChecker { constructor(){}; check(val){ if(val === 1) { return false; } } } |
I gotowe. Jeżeli teraz uruchomię npm test, przechodzić będą dwa testy. Kolejny krok to sprawienie, że kod zwracać będzie true gdy przekazana zostanie mu liczba pierwsza. Najlepszym sposobem na sprawdzenie tego jest iterowanie po liczbach od dwójki do pierwiastka ze sprawdzanej. Jeżeli któraś z liczb podzieli sprawdzaną bez reszty, to nie mamy liczby pierwszej (dlaczego szukam do pierwiastka a nie do całej liczby? Nie będę tu tłumaczył, warto poczytać w necie. W skrócie: tak jest szybciej 😛 ).
Mogę teraz uzupełnić kod w index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
export class PrimeChecker { constructor(){}; check(val){ if(val === 1) { return false; } else { for(var i = 2; i < Math.sqrt(val); i++) { if(val % i === 0) { return false; } } return true; } } } |
Dzięki tej zmianie, dwa kolejne testy zapalają się na zielono 🙂 . Ostatnią rzeczą jaką muszę dopisać to zgłaszanie błędu, gdy metoda otrzyma niepoprawne dane. To będzie bardzo szybka zmiana:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export class PrimeChecker { constructor(){}; check(val){ if(isNaN(val) || val === undefined){ throw new Error('Invalid argument') } else { if(val === 1) { return false; } else { for(var i = 2; i < Math.sqrt(val); i++) { if(val % i === 0) { return false; } } return true; } } } } |
I gotowe. A oto wynik:
Wszystko na zielono, jeden z najpiękniejszych widoków jaki można ujrzeć 🙂 .
I to wszystko. Jak widać nie ma nic trudnego w pisaniu testów jednostkowych. Oczywiście powyższy przykład był bardzo prosty, ale większość zasad przedstawionych tutaj wystarczy do testowania również bardziej skomplikowanego kodu. W kolejnych wpisach pokażę jak radzić sobie podczas testowania kodu asynchronicznego, oraz kodu z promisami. Zanim jednak do tego przejdę, będę musiał napisać o jeszcze jednym ważnym aspekcie, czyli o mierzeniu pokrycia kodu testami. Ale to w kolejnych częściach.
Na dziś to tyle. Jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem 🙂 .