Polski

Opanuj zaawansowane wzorce testowania w Jest, by tworzyć niezawodne oprogramowanie. Odkryj mockowanie, testy migawkowe, niestandardowe matchery i więcej.

Jest: Zaawansowane wzorce testowania dla solidnego oprogramowania

W dzisiejszym dynamicznym świecie tworzenia oprogramowania, zapewnienie niezawodności i stabilności bazy kodu jest sprawą nadrzędną. Chociaż Jest stał się de facto standardem do testowania JavaScript, wyjście poza podstawowe testy jednostkowe otwiera nowy poziom zaufania do aplikacji. Ten wpis zagłębia się w zaawansowane wzorce testowania w Jest, które są niezbędne do tworzenia solidnego oprogramowania, z myślą o globalnej społeczności programistów.

Dlaczego warto wyjść poza podstawowe testy jednostkowe?

Podstawowe testy jednostkowe weryfikują pojedyncze komponenty w izolacji. Jednak rzeczywiste aplikacje to złożone systemy, w których komponenty wchodzą ze sobą w interakcje. Zaawansowane wzorce testowania radzą sobie z tymi złożonościami, umożliwiając nam:

Opanowanie mockowania i szpiegów (spies)

Mockowanie jest kluczowe do izolowania testowanej jednostki poprzez zastępowanie jej zależności kontrolowanymi zamiennikami. Jest dostarcza do tego potężnych narzędzi:

jest.fn(): Podstawa mocków i szpiegów

jest.fn() tworzy funkcję-mock. Można śledzić jej wywołania, argumenty i zwracane wartości. Jest to podstawowy element do budowania bardziej zaawansowanych strategii mockowania.

Przykład: Śledzenie wywołań funkcji

// component.js
export const fetchData = () => {
  // Symuluje wywołanie API
  return Promise.resolve({ data: 'some data' });
};

export const processData = async (fetcher) => {
  const result = await fetcher();
  return `Processed: ${result.data}`;
};

// component.test.js
import { processData } from './component';

test('should process data correctly', async () => {
  const mockFetcher = jest.fn().mockResolvedValue({ data: 'mocked data' });
  const result = await processData(mockFetcher);
  expect(result).toBe('Processed: mocked data');
  expect(mockFetcher).toHaveBeenCalledTimes(1);
  expect(mockFetcher).toHaveBeenCalledWith();
});

jest.spyOn(): Obserwowanie bez zastępowania

jest.spyOn() pozwala obserwować wywołania metody na istniejącym obiekcie, niekoniecznie zastępując jej implementację. W razie potrzeby można również zamockować implementację.

Przykład: Szpiegowanie metody modułu

// logger.js
export const logInfo = (message) => {
  console.log(`INFO: ${message}`);
};

// service.js
import { logInfo } from './logger';

export const performTask = (taskName) => {
  logInfo(`Starting task: ${taskName}`);
  // ... logika zadania ...
  logInfo(`Task ${taskName} completed.`);
};

// service.test.js
import { performTask } from './service';
import * as logger from './logger';

test('should log task start and completion', () => {
  const logSpy = jest.spyOn(logger, 'logInfo');

  performTask('backup');

  expect(logSpy).toHaveBeenCalledTimes(2);
  expect(logSpy).toHaveBeenCalledWith('Starting task: backup');
  expect(logSpy).toHaveBeenCalledWith('Task backup completed.');

  logSpy.mockRestore(); // Ważne, aby przywrócić oryginalną implementację
});

Mockowanie importów modułów

Możliwości mockowania modułów w Jest są rozległe. Można mockować całe moduły lub konkretne eksporty.

Przykład: Mockowanie zewnętrznego klienta API

// api.js
import axios from 'axios';

export const getUser = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

// user-service.js
import { getUser } from './api';

export const getUserFullName = async (userId) => {
  const user = await getUser(userId);
  return `${user.firstName} ${user.lastName}`;
};

// user-service.test.js
import { getUserFullName } from './user-service';
import * as api from './api';

// Mockuj cały moduł api
jest.mock('./api');

test('should get full name using mocked API', async () => {
  // Mockuj konkretną funkcję z zamockowanego modułu
  api.getUser.mockResolvedValue({ id: 1, firstName: 'Ada', lastName: 'Lovelace' });

  const fullName = await getUserFullName(1);

  expect(fullName).toBe('Ada Lovelace');
  expect(api.getUser).toHaveBeenCalledTimes(1);
  expect(api.getUser).toHaveBeenCalledWith(1);
});

Automatyczne mockowanie a mockowanie ręczne

Jest automatycznie mockuje moduły Node.js. Dla modułów ES lub niestandardowych modułów może być potrzebne jest.mock(). Aby uzyskać większą kontrolę, można tworzyć katalogi __mocks__.

Implementacje mocków

Można dostarczać niestandardowe implementacje dla swoich mocków.

Przykład: Mockowanie z niestandardową implementacją

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// calculator.js
import { add, subtract } from './math';

export const calculate = (operation, a, b) => {
  if (operation === 'add') {
    return add(a, b);
  } else if (operation === 'subtract') {
    return subtract(a, b);
  }
  return null;
};

// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';

// Mockuj cały moduł math
jest.mock('./math');

test('should perform addition using mocked math add', () => {
  // Dostarcz implementację mocka dla funkcji 'add'
  math.add.mockImplementation((a, b) => a + b + 10); // Dodaj 10 do wyniku
  math.subtract.mockReturnValue(5); // Zamockuj również subtract

  const result = calculate('add', 5, 3);

  expect(math.add).toHaveBeenCalledWith(5, 3);
  expect(result).toBe(18); // 5 + 3 + 10

  const subResult = calculate('subtract', 10, 2);
  expect(math.subtract).toHaveBeenCalledWith(10, 2);
  expect(subResult).toBe(5);
});

Testy migawkowe (Snapshot Testing): Utrwalanie UI i konfiguracji

Testy migawkowe to potężna funkcja do przechwytywania wyników działania komponentów lub konfiguracji. Są one szczególnie przydatne do testowania interfejsu użytkownika lub weryfikacji złożonych struktur danych.

Jak działają testy migawkowe

Przy pierwszym uruchomieniu testu migawkowego, Jest tworzy plik .snap zawierający serializowaną reprezentację testowanej wartości. Przy kolejnych uruchomieniach, Jest porównuje bieżący wynik z zapisaną migawką. Jeśli się różnią, test kończy się niepowodzeniem, informując o niezamierzonych zmianach. Jest to nieocenione przy wykrywaniu regresji w komponentach UI w różnych regionach czy lokalizacjach.

Przykład: Tworzenie migawki komponentu React

Zakładając, że masz komponent React:

// UserProfile.js
import React from 'react';

const UserProfile = ({ name, email, isActive }) => (
  <div>
    <h2>{name}</h2>
    <p><strong>Email:</strong> {email}</p>
    <p><strong>Status:</strong> {isActive ? 'Aktywny' : 'Nieaktywny'}</p>
  </div>
);

export default UserProfile;

// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // Do migawek komponentów React
import UserProfile from './UserProfile';

test('renders UserProfile correctly', () => {
  const user = {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
    isActive: true,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

test('renders inactive UserProfile correctly', () => {
  const user = {
    name: 'John Smith',
    email: 'john.smith@example.com',
    isActive: false,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot('inactive user profile'); // Nazwana migawka
});

Po uruchomieniu testów, Jest utworzy plik UserProfile.test.js.snap. Kiedy zaktualizujesz komponent, będziesz musiał przejrzeć zmiany i ewentualnie zaktualizować migawkę, uruchamiając Jest z flagą --updateSnapshot lub -u.

Dobre praktyki dla testów migawkowych

Niestandardowe matchery: Poprawa czytelności testów

Wbudowane matchery w Jest są obszerne, ale czasami trzeba zweryfikować specyficzne warunki, które nie są objęte. Niestandardowe matchery pozwalają tworzyć własną logikę asercji, czyniąc testy bardziej wyrazistymi i czytelnymi.

Tworzenie niestandardowych matcherów

Możesz rozszerzyć obiekt expect Jesta o własne matchery.

Przykład: Sprawdzanie poprawności formatu adresu e-mail

W pliku konfiguracyjnym Jest (np. jest.setup.js, skonfigurowanym w jest.config.js):

// jest.setup.js

expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () => `expected ${received} not to be a valid email`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be a valid email`,
        pass: false,
      };
    }
  },
});

// W twoim pliku jest.config.js
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };

W pliku testowym:

// validation.test.js

test('should validate email formats', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
  expect('another.test@sub.domain.co.uk').toBeValidEmail();
});

Korzyści z niestandardowych matcherów

Testowanie operacji asynchronicznych

JavaScript jest w dużej mierze asynchroniczny. Jest zapewnia doskonałe wsparcie do testowania promisów i async/await.

Używanie async/await

To nowoczesny i najbardziej czytelny sposób testowania kodu asynchronicznego.

Przykład: Testowanie funkcji asynchronicznej

// dataService.js
export const fetchUserData = async (userId) => {
  // Symuluje pobieranie danych po opóźnieniu
  await new Promise(resolve => setTimeout(resolve, 50));
  if (userId === 1) {
    return { id: 1, name: 'Alice' };
  } else {
    throw new Error('User not found');
  }
};

// dataService.test.js
import { fetchUserData } from './dataService';

test('fetches user data correctly', async () => {
  const user = await fetchUserData(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user', async () => {
  await expect(fetchUserData(2)).rejects.toThrow('User not found');
});

Używanie .resolves i .rejects

Te matchery upraszczają testowanie rozwiązywania i odrzucania promisów.

Przykład: Używanie .resolves/.rejects

// dataService.test.js (kontynuacja)

test('fetches user data with .resolves', () => {
  return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user with .rejects', () => {
  return expect(fetchUserData(2)).rejects.toThrow('User not found');
});

Obsługa timerów

Dla funkcji używających setTimeout lub setInterval, Jest zapewnia kontrolę nad timerami.

Przykład: Kontrolowanie timerów

// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
  setTimeout(() => {
    callback(`Hello, ${name}!`);
  }, 1000);
};

// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';

jest.useFakeTimers(); // Włącz fałszywe timery

test('greets after delay', () => {
  const mockCallback = jest.fn();
  greetAfterDelay('World', mockCallback);

  // Przesuń timery o 1000ms
  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
  expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});

// Przywróć prawdziwe timery, jeśli są potrzebne gdzie indziej
jest.useRealTimers();

Organizacja i struktura testów

W miarę rozrastania się zestawu testów, organizacja staje się kluczowa dla łatwości utrzymania.

Bloki `describe` i `it`

Używaj describe do grupowania powiązanych testów i it (lub test) dla poszczególnych przypadków testowych. Ta struktura odzwierciedla modułowość aplikacji.

Przykład: Ustrukturyzowane testy

describe('User Authentication Service', () => {
  let authService;

  beforeEach(() => {
    // Ustaw mocki lub instancje serwisów przed każdym testem
    authService = require('./authService');
    jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
  });

  afterEach(() => {
    // Wyczyść mocki
    jest.restoreAllMocks();
  });

  describe('login functionality', () => {
    it('should successfully log in a user with valid credentials', async () => {
      const result = await authService.login('user@example.com', 'password123');
      expect(result.token).toBeDefined();
      // ... więcej asercji ...
    });

    it('should fail login with invalid credentials', async () => {
      jest.spyOn(authService, 'login').mockRejectedValue(new Error('Invalid credentials'));
      await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Invalid credentials');
    });
  });

  describe('logout functionality', () => {
    it('should clear user session', async () => {
      // Testowanie logiki wylogowania...
    });
  });
});

Hooki `setup` i `teardown`

Te hooki są niezbędne do przygotowywania danych-mocków, połączeń z bazą danych lub czyszczenia zasobów między testami.

Testowanie dla globalnej publiczności

Podczas tworzenia aplikacji dla globalnej publiczności, zagadnienia testowe rozszerzają się:

Internacjonalizacja (i18n) i lokalizacja (l10n)

Upewnij się, że twój interfejs użytkownika i komunikaty poprawnie dostosowują się do różnych języków i formatów regionalnych.

Przykład: Testowanie formatowania zlokalizowanej daty

// dateUtils.js
export const formatLocalizedDate = (date, locale) => {
  return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(date);
};

// dateUtils.test.js
import { formatLocalizedDate } from './dateUtils';

test('formats date correctly for US locale', () => {
  const date = new Date(2023, 10, 15); // 15 listopada 2023
  expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});

test('formats date correctly for German locale', () => {
  const date = new Date(2023, 10, 15);
  expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});

Świadomość stref czasowych

Testuj, jak twoja aplikacja obsługuje różne strefy czasowe, szczególnie w przypadku funkcji takich jak planowanie czy aktualizacje w czasie rzeczywistym. Mockowanie zegara systemowego lub używanie bibliotek abstrahujących strefy czasowe może być korzystne.

Kulturowe niuanse w danych

Zastanów się, jak liczby, waluty i inne reprezentacje danych mogą być postrzegane lub oczekiwane inaczej w różnych kulturach. Niestandardowe matchery mogą być tutaj szczególnie przydatne.

Zaawansowane techniki i strategie

Test-Driven Development (TDD) i Behavior-Driven Development (BDD)

Jest dobrze współgra z metodologiami TDD (Red-Green-Refactor) i BDD (Given-When-Then). Pisz testy opisujące pożądane zachowanie przed napisaniem kodu implementacji. Zapewnia to, że kod jest pisany z myślą o testowalności od samego początku.

Testy integracyjne z Jest

Chociaż Jest doskonale sprawdza się w testach jednostkowych, może być również używany do testów integracyjnych. Mockowanie mniejszej liczby zależności lub używanie narzędzi takich jak opcja runInBand w Jest może pomóc.

Przykład: Testowanie interakcji z API (uproszczone)

// apiService.js
import axios from 'axios';

const API_BASE_URL = 'https://api.example.com';

export const createProduct = async (productData) => {
  const response = await axios.post(`${API_BASE_URL}/products`, productData);
  return response.data;
};

// apiService.test.js (Test integracyjny)
import axios from 'axios';
import { createProduct } from './apiService';

// Mockuj axios dla testów integracyjnych, aby kontrolować warstwę sieciową
jest.mock('axios');

test('creates a product via API', async () => {
  const mockProduct = { id: 1, name: 'Gadget' };
  const responseData = { success: true, product: mockProduct };

  axios.post.mockResolvedValue({
    data: responseData,
    status: 201,
    headers: { 'content-type': 'application/json' },
  });

  const newProductData = { name: 'Gadget', price: 99.99 };
  const result = await createProduct(newProductData);

  expect(axios.post).toHaveBeenCalledWith(`${process.env.API_BASE_URL || 'https://api.example.com'}/products`, newProductData);
  expect(result).toEqual(responseData);
});

Równoległość i konfiguracja

Jest może uruchamiać testy równolegle, aby przyspieszyć ich wykonanie. Skonfiguruj to w pliku jest.config.js. Na przykład, ustawienie maxWorkers kontroluje liczbę równoległych procesów.

Raporty pokrycia kodu (coverage)

Używaj wbudowanego w Jest raportowania pokrycia, aby zidentyfikować części bazy kodu, które nie są testowane. Uruchom testy z flagą --coverage, aby wygenerować szczegółowe raporty.

jest --coverage

Przeglądanie raportów pokrycia pomaga upewnić się, że twoje zaawansowane wzorce testowania skutecznie pokrywają krytyczną logikę, w tym ścieżki kodu związane z internacjonalizacją i lokalizacją.

Podsumowanie

Opanowanie zaawansowanych wzorców testowania w Jest to znaczący krok w kierunku budowania niezawodnego, łatwego w utrzymaniu i wysokiej jakości oprogramowania dla globalnej publiczności. Efektywnie wykorzystując mockowanie, testy migawkowe, niestandardowe matchery i techniki testowania asynchronicznego, możesz zwiększyć solidność swojego zestawu testów i zyskać większą pewność co do zachowania aplikacji w różnych scenariuszach i regionach. Przyjęcie tych wzorców wzmacnia zespoły programistyczne na całym świecie w dostarczaniu wyjątkowych doświadczeń użytkownika.

Zacznij wdrażać te zaawansowane techniki do swojego przepływu pracy już dziś, aby podnieść swoje praktyki testowania JavaScript na wyższy poziom.