Narzędzia branżowe

Zasadniczo testy automatyczne to po prostu kod, który wysyła lub powoduje błąd w przypadku niepowodzenia. Większość bibliotek i platform testowych udostępnia różne podstawowe elementy, które ułatwiają pisanie testów.

Jak wspomnieliśmy w poprzedniej sekcji, te podstawowe elementy prawie zawsze umożliwiają definiowanie niezależnych testów (nazywanych przypadkami testowymi) i tworzenie asercji. Asercje to sposób na łączenie sprawdzania wyniku i zgłaszania błędu, jeśli coś jest nie w porządku. Można je uznać za podstawowe elementy podstawowe testowania.

Na tej stronie przedstawiono ogólne podejście do tych podstawowych elementów. Wybrana platforma prawdopodobnie ma podobny wygląd, ale nie jest to dokładny odniesienie.

Na przykład:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

W tym przykładzie tworzymy grupę testów (czasami nazywanych pakietem) o nazwie „testy matematyczne” i definiujemy 3 niezależne przypadki testowe, z których każdy wykonuje pewne asercje. Takie przypadki testowe można zwykle adresować pojedynczo lub uruchamiać za pomocą flagi filtra w narzędziu do uruchamiania testów.

Elementy pomocnicze asercji jako elementy podstawowe

Większość platform testowych, w tym Vitest, zawiera kolekcję elementów wspomagających asercję w obiekcie assert, co pozwala szybko sprawdzić zwracane wartości lub inne stany pod kątem określonych expectation. Oczekiwania te są często wartościami „znanymi dobrymi”. W poprzednim przykładzie 13. liczba Fibonacci powinna wynosić 233, więc możemy to potwierdzić bezpośrednio za pomocą funkcji assert.equal.

Możesz też oczekiwać, że wartość ma określoną formę, jest wyższa od innej wartości lub ma inną właściwość. Ten kurs nie obejmuje pełnego zakresu możliwych elementów pomocniczych asercji, ale platformy do testowania zawsze zapewniają co najmniej te podstawowe elementy kontrolne:

  • Kontrola 'truthy', często nazywana „OK”, sprawdza, czy dany warunek jest spełniony. Odpowiada ona zapisowi if, który sprawdza, czy coś się uda lub jest poprawne. Zwykle jest podawany jako assert(...) lub assert.ok(...) i przyjmuje 1 wartość oraz opcjonalny komentarz.

  • Kontrola równości, np. w przykładzie testu matematycznego, w którym spodziewasz się, że zwrócona wartość lub stan obiektu będą równe znanej wartości. Służą one do podstawowych równości (np. w przypadku liczb i ciągów znaków) lub równości odniesienia (to ten sam obiekt). Są to jedynie porównanie wiarygodności funkcji == lub ===.

    • JavaScript rozróżnia luźne (==) i rygorystyczne (===). Większość bibliotek testowych udostępnia metody assert.equal i assert.strictEqual.
  • Szczegółowa kontrola równości, która poszerza sprawdzanie równości o sprawdzanie zawartości obiektów, tablic i innych bardziej złożonych typów danych, a także z wewnętrzną logiką, która umożliwia przemierzanie obiektów w celu ich porównania. Są one ważne, ponieważ JavaScript nie ma wbudowanego sposobu porównywania zawartości 2 obiektów lub tablic. Na przykład [1,2,3] == [1,2,3] ma zawsze wartość fałsz. Platformy testowe często zawierają pomocników deepEqual lub deepStrictEqual.

Asystent asercji, który porównuje 2 wartości (a nie tylko test „prawda”), zwykle przyjmują 2 lub 3 argumenty:

  • Rzeczywista wartość wygenerowana na podstawie testowanego kodu lub opisującego stan do sprawdzenia.
  • Oczekiwana wartość, zwykle zakodowana na stałe (np. liczba literału lub ciąg znaków).
  • Opcjonalny komentarz opisujący, co jest normalne, a co mogło się nie udać. Dołączy się, jeśli ten wiersz nie powiedzie się.

Dość powszechną praktyką jest łączenie asercji w celu utworzenia różnego rodzaju kontroli, ponieważ rzadko potrafi się prawidłowo potwierdzić stan systemu. Na przykład:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest korzysta wewnętrznie z biblioteki asercji Chai, aby udostępniać swoich pomocników do zgłaszania roszczeń. Może ona przydać się do sprawdzenia, jakie asercje i asercje mogą pasować do Twojego kodu.

płynne wypowiedź i BDD.

Niektórzy deweloperzy preferują styl asercji, który można nazywać asercjami opartymi na zachowaniach (BDD) lub asercjami w stylu elastycznym. Są one również nazywane „oczekiwaniami”, ponieważ punktem wejścia do sprawdzania oczekiwań jest metoda o nazwie expect().

Oczekuje się, że pomocnicy będą zachowywać się w taki sam sposób jak asercje zapisane jako proste wywołania metody, np. assert.ok lub assert.strictDeepEquals, ale niektórzy deweloperzy uważają je za bardziej czytelne. Potwierdzenie BDD może wyglądać tak:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Te style asercji działają dzięki technice nazywanej łańcuchem metod, w której obiekt zwrócony przez expect może być stale łączony z kolejnymi wywołaniami metod. Niektóre części wywołania, w tym to.be i that.does w poprzednim przykładzie, nie mają żadnej funkcji i są uwzględniane tylko po to, by ułatwić odczytanie wywołania i potencjalnie wygenerować automatyczny komentarz, jeśli test się nie powiedzie. expect zwykle nie obsługuje opcjonalnego komentarza, bo łańcuch powinien jasno opisywać błąd.

Wiele platform do testów obsługuje zarówno rozwiązania Fluent/BDD, jak i regularne asercje. Vitestna przykład eksportuje oba podejścia i ma nieco bardziej zwięzłe podejście do BDD. Jest z kolei domyślnie zawiera tylko metodę przewidywaną.

Grupowanie testów w plikach

Już podczas pisania testów stosujemy grupowanie niejawne. Zamiast wszystkich testów w jednym pliku zwykle zapisujemy je w wielu plikach. Testerzy zwykle wiedzą, że plik jest przeznaczony do testów dzięki wstępnie zdefiniowanemu filtrowi lub wyrażeniu regularnemu. Na przykład Vitest obejmuje wszystkie pliki w projekcie, które kończą się rozszerzeniem „.test.jsx” lub „.spec.ts” („.test” i „.spec” oraz kilkoma prawidłowymi rozszerzeniami).

Testy komponentów znajdują się zwykle w pliku równorzędnym z testowanym komponentem, jak w takiej strukturze katalogów:

Lista plików w katalogu, w tym UserList.tsx i UserList.test.tsx.
Plik komponentu i powiązany plik testowy.

Podobnie testy jednostkowe są zwykle umieszczane obok testowanego kodu. Kompleksowe testy mogą znajdować się w osobnym pliku, a testy integracji mogą znajdować się nawet w osobnych folderach. Takie struktury mogą być przydatne, gdy złożone przypadki testowe wymagają własnych plików pomocniczych, które nie są testami, np. bibliotek pomocniczych potrzebnych do przeprowadzenia testu.

Grupowanie testów w plikach

Tak jak w poprzednich przykładach, częste jest umieszczanie testów w wywołaniu funkcji suite(), które grupuje testy skonfigurowane za pomocą test(). Pakiety zwykle nie są testami, ale pomagają w uporządkowaniu struktury przez grupowanie powiązanych testów lub celów przez wywołanie prawidłowej metody. W przypadku test() przekazywana metoda opisuje działania samego testu.

W przypadku testów Fluent i BDD istnieje dość standardowy odpowiednik testów grupowania. Oto niektóre typowe przykłady w tym kodzie:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

W większości platform suite i describe zachowują się podobnie jak test i it, ale występują większe różnice między pisaniem asercji przy użyciu expect i assert.

Inne narzędzia mają nieco odmienne sposoby organizowania zestawów i testów. Na przykład wbudowany mechanizm uruchamiania testów w Node.js obsługuje zagnieżdżanie wywołań test() w celu niejawnego utworzenia hierarchii testowej. Vitest zezwala jednak na tego rodzaju zagnieżdżanie tylko za pomocą atrybutu suite() i nie uruchamia metody test() zdefiniowanej w innym obiekcie test().

Tak jak w przypadku asercji, pamiętaj, że dokładna kombinacja metod grupowania dostępnych w stosie technologicznym nie jest aż tak ważna. W tym kursie zostaną one omówione w całości, ale musisz zorientować się, jak mają się one do wybranych przez Ciebie narzędzi.

Metody cyklu życia

Jednym z powodów, dla których warto grupować testy, nawet pośrednio na najwyższym poziomie w pliku, jest podanie metod konfiguracji i demontażu, które są stosowane dla każdego testu lub raz dla grupy testów. Większość formatów udostępnia 4 metody:

Dla każdej funkcji „test()” lub „it()” Raz w pokoju
Przed uruchomieniem testów „beforeEach()” „beforeAll()”
Po uruchomieniach testowych „afterEach()” „afterAll()”

Na przykład przed każdym testem możesz wstępnie wypełnić wirtualną bazę danych użytkowników i wyczyścić ją później:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Pozwala to uprościć testy. Zamiast powielać go w każdym teście, możesz udostępniać wspólny kod konfiguracji i usuwania treści. Dodatkowo, jeśli sama konfiguracja i kod demontażu zwracają błąd, może to wskazywać na problemy strukturalne, które nie obejmują niepowodzenia samych testów.

Porady ogólne

Oto kilka wskazówek, o których warto pamiętać, rozważając te podstawowe elementy.

Podstawowe zasady

Pamiętaj, że narzędzia i elementy podstawowe opisane tutaj i na kilku następnych stronach nie będą dokładnie odpowiadać Vitest, Jest, Mocha, Web Test Runner ani żadnej innej platformie. Użyliśmy Vitest jako ogólnego przewodnika, ale pamiętaj, aby zmapować go do wybranej platformy.

Mieszaj i dopasowuj asercje według potrzeb

Testy to zasadniczo kod, który może powodować błędy. Każdy element biegowy dostarczy element podstawowy (prawdopodobnie test()), który będzie służyć do opisywania różnych przypadków testowych.

Jeśli ten biegacz udostępnia też elementy assert(), expect() i pomoce asercji, pamiętaj, że ta część to przede wszystkim wygoda. W razie potrzeby możesz ją pominąć. Możesz uruchomić dowolny kod, który może powodować błąd, w tym inne biblioteki asercji lub instrukcję if w starym stylu.

Konfiguracja IDE może uratować życie

Sprawdzenie, czy Twoje IDE, takie jak VSCode, ma dostęp do autouzupełniania i dokumentacji wybranego narzędzia testowego, może zwiększyć Twoją produktywność. Na przykład w bibliotece asercji Chai istnieje ponad 100 metod w metodzie assert, a dokumentacja właściwej metody jest wygodna.

Może to być szczególnie ważne w przypadku niektórych platform testowych, które zapełniają globalną przestrzeń nazw za pomocą swoich metod testowania. Jest to niewielka różnica, ale często można użyć bibliotek testowych bez ich importowania, jeśli zostaną one automatycznie dodane do globalnej przestrzeni nazw:

// some.test.js
test('using test as a global', () => { … });

Zalecamy zaimportowanie obiektów pomocniczych, nawet jeśli są obsługiwane automatycznie, ponieważ dzięki temu IDE będzie mieć jasny sposób wyszukiwania tych metod. Być może zdarzyło Ci się napotkać ten problem podczas tworzenia React, ponieważ niektóre bazy kodu mają magiczny globalny React, a inne nie. W związku z tym trzeba go zaimportować we wszystkich plikach za pomocą Reacta.

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });