front-end

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 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 ===). Dlatego 5 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!

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)