Wprowadzanie do testów jednostkowych w JEST (Javascript + Typescript) #1
Nie zna swojego kodu ten, kto testów nie pisze!
Dzisiaj rozpoczniemy zabawę z testami jednostkowymi. Już trochę doświadczenia z JESTem mam, ale zaczniemy zgodnie z naturalnym biegiem spraw... Od początku!
Test jednostkowe (tzw. unit testy) są to testy sprawdzające malutkie części kodu: najczęściej funkcje lub/i metody. Każdy test jest niezależny od poprzedniego oraz w jak najmniejszym stopniu opiera się na zewnętrznych zależnościach (wręcz nie powinno ich być!). Test może zakończyć się sukcesem lub porażką (ang. fail).
npm i --save-dev jest
to dobry początek, potem w package.json można podpiąć npm test
do polecenia jest
, którym uruchamiamy testy.
Tworzymy folder tests
, a potem plik mytest.test.js.
Nazewnictwo jest tutaj kluczowe, bowiem domyślnie JEST szuka testów w wszystkich plikach w projekcie o rozszerzeniu *.test.js. Jest to szalenie wygodne.
Odpalamy!
Teraz sprawdźmy czy wszystko działa :)
W pliku mytest.test.js utwórzmy sobie test, który zawsze będzie przechodził. W JEST testy definiuje się bardzo prosto: funkcją test
.
Jej pierwszym argumentem jest opis testu, kolejnym funkcja w której wywołujemy expect
sprawdzającą poprawność wartości. W tej chwili pominiemy wszystkie inne aspekty.
test('3 is equal to 3', () => {
expect(3).toBe(3);
});
Powyższy test sprawcza czy 3 === 3
.
Teraz w konsoli puszczamy komendę jest
lub `npm test`, jeśli wpisaliśmy polecenie do package.jsona. Wynik?
> storyportal@1.0.0 test /home/kamila/Dokumenty/StoryPortal
> jest
PASS tests/first.test.js
✓ adds 1 + 2 to equal 3 (6ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.332s
Ran all test suites.
Pełen sukces!
Chociaż przyznam, że programistyczny instynkt podpowiedział mi, że 3 jest równie 3 :)
Typescript
Niestety dobrze wiecie, że to nie koniec konfiguracji bowiem moja miłość do TypeScripta nie pozwoliłaby mi spać. Musicie wiedzieć, że są dwa sposoby na integrację JESTa z Typescriptem, jeden sprawdza typy, drugi korzysta z wygenerowanych plików js. My oczywiście chcemy sprawdzać typy, więc użyjemy ts-jest!
Najpierw instalacja:
npm i --save-dev jest typescript
npm i --save-dev ts-jest @types/jest
npx ts-jest config:init
Teraz stworzymy plik typetest.test.ts
i wklejamy dokładnie to co mieliśmy w poprzednim teście.
I puszczamy npm test
. Wynik?
> storyportal@1.0.0 test /home/kamila/Dokumenty/StoryPortal
> jest
PASS tests/first.test.js
PASS tests/typetest.test.ts
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.643s, estimated 5s
Ran all test suites.
Znowu dziki, programistyczny instynkt zadziałał i byłam niemal pewna, że cztery jest równe cztery. Niemal :)
No ale w końcu musimy coś przetestować i dlatego wybrałam pusty controller.
import { JsonController } from "routing-controllers";
@JsonController()
export class StoryController {
constructor() {
console.log('Wywołany!');
}
}
Stwórzmy plik storyController.test.ts i wypełnijmy go szalenie nudnym testem. Sprawdzimy czy zmienna do której przypisaliśmy nową instancję klasy na pewno jest instancji tej klasy. W prostych aplikacjach jest to oczywiście kompletna bzdura.
import { StoryController } from './../src/controllers/StoryController';
test('StoryController constructor allows creating story', () => {
const storyController = new StoryController();
expect(storyController).toBeInstanceOf(StoryController);
});
Puszczamy npm test
i co? Błąd. Okazuje się, że JEST nie radzi sobie z czytaniem relatywnych ścieżek z aplikacji, które są zdefiniowane w tsconfig.json.
Problem jest powszechnie znany i powstał fix naprawiający go w pewnym stopniu: link, jednak nie wystarczająco dobry jak na moją aplikację, gdzie trochę podmieniam nazwy ścieżek.
Istnieją dwa wyjścia:
- ręczne wpisywanie mapperów do jest.config.js
- zastosowanie jakieś funkcji/biblioteki np.: https://www.npmjs.com/package/jest-module-name-mapper
Ręczne wpisywanie jest prostsze, ale wymaga duplikacji danych co zawsze wiąże się z ryzykiem starty spójności. Dlatego zainstalujemy i użyjemy jest-module-name-mapper
:)
npm i --save-dev jest-module-name-mapper
A w jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: require('jest-module-name-mapper')(),
};
Puszczamy... I działa! Ale zaszaleliśmy!
Expect
Czeka nas jeszcze długa droga poprzez mockowanie, fake'owanie, strukturę JESTa i wiele innych przygód.
Na żadną z nich nie wyruszymy bez przybliżenia się funkcji expect
.
Zwraca ona obiekt z metodami, które umożliwiają wybranie wygodnego operatora logicznego oraz tego co dokładnie będziemy porównywać.
W poprzednich przykładach poznaliśmy dwie z tych metod: toBe
(która jest jednym z kilku odpowiedników równości w JEST) oraz beInstanceOf (która sprawdza klasę zmiennej).
Teraz napiszemy kilka oczywistych testów, żeby poznać bliżej JESTa. Wszystkie testy poniżej przechodzą.
test('if 3 is equal to 3', () => {
expect(5).not.toBe('5');
});
test('if 3 is equal to 3', () => {
expect(0).toBeFalsy();
});
test('if 3 is equal to 3', () => {
expect(5).toBeLessThan(6);
});
test('if 3 is equal to 3', () => {
expect(5).toBeGreaterThanOrEqual(5);
});
test('if 3 is equal to 3', () => {
// strict checking
expect(['cat', 5]).toContain('cat');
});
test('if 3 is equal to 3', () => {
expect(4).not.toBe(3);
});
Z powyższych przykładów powinniście zapamiętać:
- funkcję
not
będącą zaprzeczeniem - niektóre funkcje sprawdzają typy strictly (z porównaniem typów, podobnie jak
===
). Dlatego5
nie jest równe'5'
. - funkcje takie jak toBeFalsy rzutują na bool'a. Jest to szczególnie przydatne przy sprawdzaniu warunków.
Podsumowanie
I jak podobało się Wam? Gotowi na więcej? Jeśli ktoś chce się podzielić uwagami na temat pisania testów w JEST lub ma inny ulubiony framework to zapraszam do sekcji komentarzy.
W następnej części przygotujcie się na testowanie funkcji oraz klas, w tym także tych asynchronicznych!