Mestr avancerede Jest-testmønstre for at bygge mere pålidelig og vedligeholdelsesvenlig software. Udforsk teknikker som mocking, snapshot-test, custom matchers og mere for globale udviklingsteams.
Jest: Avancerede testmønstre for robust software
I nutidens tempofyldte softwareudviklingslandskab er det altafgørende at sikre pålideligheden og stabiliteten af din kodebase. Selvom Jest er blevet en de facto-standard for JavaScript-test, åbner det at gå ud over simple enhedstests op for et nyt niveau af tillid til dine applikationer. Dette indlæg dykker ned i avancerede Jest-testmønstre, der er essentielle for at bygge robust software, og henvender sig til et globalt publikum af udviklere.
Hvorfor gå ud over simple enhedstests?
Simple enhedstests verificerer individuelle komponenter i isolation. Men virkelige applikationer er komplekse systemer, hvor komponenter interagerer. Avancerede testmønstre håndterer disse kompleksiteter ved at gøre os i stand til at:
- Simulere komplekse afhængigheder.
- Fange UI-ændringer pålideligt.
- Skrive mere udtryksfulde og vedligeholdelsesvenlige tests.
- Forbedre testdækning og tillid til integrationspunkter.
- Fremme arbejdsgange med testdrevet udvikling (TDD) og adfærdsdrevet udvikling (BDD).
Mestring af Mocking og Spies
Mocking er afgørende for at isolere den enhed, der testes, ved at erstatte dens afhængigheder med kontrollerede substitutter. Jest leverer kraftfulde værktøjer til dette:
jest.fn()
: Grundlaget for Mocks og Spies
jest.fn()
opretter en mock-funktion. Du kan spore dens kald, argumenter og returværdier. Dette er byggestenen for mere sofistikerede mocking-strategier.
Eksempel: Sporing af funktionskald
// component.js
export const fetchData = () => {
// Simulates an API call
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()
: Observer uden at erstatte
jest.spyOn()
giver dig mulighed for at observere kald til en metode på et eksisterende objekt uden nødvendigvis at erstatte dens implementering. Du kan også mocke implementeringen, hvis det er nødvendigt.
Eksempel: Spionering på en modulmetode
// logger.js
export const logInfo = (message) => {
console.log(`INFO: ${message}`);
};
// service.js
import { logInfo } from './logger';
export const performTask = (taskName) => {
logInfo(`Starting task: ${taskName}`);
// ... task logic ...
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(); // Vigtigt at gendanne den oprindelige implementering
});
Mocking af modulimports
Jests muligheder for modul-mocking er omfattende. Du kan mocke hele moduler eller specifikke eksporteringer.
Eksempel: Mocking af en ekstern API-klient
// 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';
// Mock hele api-modulet
jest.mock('./api');
test('should get full name using mocked API', async () => {
// Mock den specifikke funktion fra det mockede modul
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);
});
Auto-mocking vs. manuel mocking
Jest mocker automatisk Node.js-moduler. For ES-moduler eller brugerdefinerede moduler kan du have brug for jest.mock()
. For mere kontrol kan du oprette __mocks__
-mapper.
Mock-implementeringer
Du kan levere brugerdefinerede implementeringer til dine mocks.
Eksempel: Mocking med en brugerdefineret implementering
// 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';
// Mock hele math-modulet
jest.mock('./math');
test('should perform addition using mocked math add', () => {
// Angiv en mock-implementering for 'add'-funktionen
math.add.mockImplementation((a, b) => a + b + 10); // Læg 10 til resultatet
math.subtract.mockReturnValue(5); // Mock også '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);
});
Snapshot-test: Bevarelse af UI og konfiguration
Snapshot-tests er en kraftfuld funktion til at fange outputtet fra dine komponenter eller konfigurationer. De er især nyttige til UI-test eller verifikation af komplekse datastrukturer.
Hvordan snapshot-test fungerer
Første gang en snapshot-test kører, opretter Jest en .snap
-fil, der indeholder en serialiseret repræsentation af den testede værdi. Ved efterfølgende kørsler sammenligner Jest det aktuelle output med det gemte snapshot. Hvis de afviger, fejler testen, hvilket advarer dig om utilsigtede ændringer. Dette er uvurderligt til at opdage regressioner i UI-komponenter på tværs af forskellige regioner eller landestandarder.
Eksempel: Snapshot af en React-komponent
Antaget at du har en React-komponent:
// 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 ? 'Active' : 'Inactive'}</p>
</div>
);
export default UserProfile;
// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // Til snapshots af React-komponenter
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'); // Navngivet snapshot
});
Efter at have kørt testene, vil Jest oprette en UserProfile.test.js.snap
-fil. Når du opdaterer komponenten, skal du gennemgå ændringerne og potentielt opdatere snapshottet ved at køre Jest med --updateSnapshot
- eller -u
-flaget.
Bedste praksis for snapshot-test
- Brug til UI-komponenter og konfigurationsfiler: Ideelt til at sikre, at UI-elementer gengives som forventet, og at konfiguration ikke ændres utilsigtet.
- Gennemgå snapshots omhyggeligt: Accepter ikke blindt snapshot-opdateringer. Gennemgå altid, hvad der er ændret, for at sikre, at ændringerne er tilsigtede.
- Undgå snapshots for data, der ændrer sig hyppigt: Hvis data ændrer sig hurtigt, kan snapshots blive skrøbelige og føre til overdreven støj.
- Brug navngivne snapshots: Til test af flere tilstande af en komponent giver navngivne snapshots bedre klarhed.
Custom Matchers: Forbedring af testlæsbarhed
Jests indbyggede matchers er omfattende, men nogle gange har du brug for at verificere specifikke betingelser, der ikke er dækket. Custom matchers giver dig mulighed for at oprette din egen assertionslogik, hvilket gør dine tests mere udtryksfulde og læsbare.
Oprettelse af Custom Matchers
Du kan udvide Jests expect
-objekt med dine egne matchers.
Eksempel: Kontrol af et gyldigt e-mailformat
I din Jest-opsætningsfil (f.eks. jest.setup.js
, konfigureret i jest.config.js
):
// jest.setup.js
expect.extend({
toBeValidEmail(received) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
if (pass) {
return {
message: () => `forventede, at ${received} ikke var en gyldig e-mail`,
pass: true,
};
} else {
return {
message: () => `forventede, at ${received} var en gyldig e-mail`,
pass: false,
};
}
},
});
// In your jest.config.js
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };
I din testfil:
// 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();
});
Fordele ved Custom Matchers
- Forbedret læsbarhed: Tests bliver mere deklarative og angiver, *hvad* der testes, frem for *hvordan*.
- Genbrug af kode: Undgå at gentage kompleks assertionslogik på tværs af flere tests.
- Domænespecifikke assertions: Tilpas assertions til din applikations specifikke domænekrav.
Test af asynkrone operationer
JavaScript er i høj grad asynkront. Jest giver fremragende understøttelse til test af promises og async/await.
Brug af async/await
Dette er den moderne og mest læsbare måde at teste asynkron kode på.
Eksempel: Test af en asynkron funktion
// dataService.js
export const fetchUserData = async (userId) => {
// Simuler hentning af data efter en forsinkelse
await new Promise(resolve => setTimeout(resolve, 50));
if (userId === 1) {
return { id: 1, name: 'Alice' };
} else {
throw new Error('Bruger ikke fundet');
}
};
// dataService.test.js
import { fetchUserData } from './dataService';
test('henter brugerdata korrekt', async () => {
const user = await fetchUserData(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
test('kaster fejl for ikke-eksisterende bruger', async () => {
await expect(fetchUserData(2)).rejects.toThrow('Bruger ikke fundet');
});
Brug af .resolves
og .rejects
Disse matchers forenkler test af promise-afviklinger og -afvisninger.
Eksempel: Brug af .resolves/.rejects
// dataService.test.js (continued)
test('henter brugerdata med .resolves', () => {
return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});
test('kaster fejl for ikke-eksisterende bruger med .rejects', () => {
return expect(fetchUserData(2)).rejects.toThrow('Bruger ikke fundet');
});
Håndtering af timere
For funktioner, der bruger setTimeout
eller setInterval
, giver Jest timerkontrol.
Eksempel: Kontrol af timere
// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
setTimeout(() => {
callback(`Hello, ${name}!`);
}, 1000);
};
// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';
jest.useFakeTimers(); // Aktivér falske timere
test('hilser efter forsinkelse', () => {
const mockCallback = jest.fn();
greetAfterDelay('World', mockCallback);
// Fremskynd timere med 1000ms
jest.advanceTimersByTime(1000);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});
// Gendan rigtige timere, hvis det er nødvendigt andre steder
jest.useRealTimers();
Testorganisering og -struktur
Efterhånden som din test-suite vokser, bliver organisering afgørende for vedligeholdelsen.
Describe-blokke og It-blokke
Brug describe
til at gruppere relaterede tests og it
(eller test
) til individuelle testcases. Denne struktur afspejler applikationens modularitet.
Eksempel: Strukturerede tests
describe('Brugergodkendelsestjeneste', () => {
let authService;
beforeEach(() => {
// Opsæt mocks eller serviceinstanser før hver test
authService = require('./authService');
jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
});
afterEach(() => {
// Ryd op i mocks
jest.restoreAllMocks();
});
describe('login-funktionalitet', () => {
it('skal logge en bruger ind med gyldige legitimationsoplysninger', async () => {
const result = await authService.login('user@example.com', 'password123');
expect(result.token).toBeDefined();
// ... flere assertions ...
});
it('skal fejle login med ugyldige legitimationsoplysninger', async () => {
jest.spyOn(authService, 'login').mockRejectedValue(new Error('Ugyldige legitimationsoplysninger'));
await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Ugyldige legitimationsoplysninger');
});
});
describe('logout-funktionalitet', () => {
it('skal rydde brugersession', async () => {
// Test logout-logik...
});
});
});
Setup- og Teardown-hooks
beforeAll
: Kører én gang før alle tests i endescribe
-blok.afterAll
: Kører én gang efter alle tests i endescribe
-blok.beforeEach
: Kører før hver test i endescribe
-blok.afterEach
: Kører efter hver test i endescribe
-blok.
Disse hooks er essentielle for at opsætte mock-data, databaseforbindelser eller rydde op i ressourcer mellem tests.
Test for et globalt publikum
Når man udvikler applikationer til et globalt publikum, udvides testovervejelserne:
Internationalisering (i18n) og lokalisering (l10n)
Sørg for, at din UI og dine meddelelser tilpasser sig korrekt til forskellige sprog og regionale formater.
- Snapshot af lokaliseret UI: Test, at forskellige sprogversioner af din UI gengives korrekt ved hjælp af snapshot-tests.
- Mocking af landestandarddata: Mock biblioteker som
react-intl
elleri18next
for at teste komponentadfærd med forskellige landestandardmeddelelser. - Formatering af dato, tid og valuta: Test, at disse håndteres korrekt ved hjælp af custom matchers eller ved at mocke internationaliseringsbiblioteker. For eksempel at verificere, at en dato formateret for Tyskland (DD.MM.YYYY) vises anderledes end for USA (MM/DD/YYYY).
Eksempel: Test af lokaliseret datoformatering
// 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('formaterer dato korrekt for US-landestandard', () => {
const date = new Date(2023, 10, 15); // 15. november 2023
expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});
test('formaterer dato korrekt for tysk landestandard', () => {
const date = new Date(2023, 10, 15);
expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});
Tidszonebevidsthed
Test, hvordan din applikation håndterer forskellige tidszoner, især for funktioner som planlægning eller realtidsopdateringer. Mocking af systemuret eller brug af biblioteker, der abstraherer tidszoner, kan være en fordel.
Kulturelle nuancer i data
Overvej, hvordan tal, valutaer og andre datarepræsentationer kan opfattes eller forventes forskelligt på tværs af kulturer. Custom matchers kan være særligt nyttige her.
Avancerede teknikker og strategier
Testdrevet udvikling (TDD) og adfærdsdrevet udvikling (BDD)
Jest passer godt sammen med TDD (Red-Green-Refactor) og BDD (Given-When-Then) metodologier. Skriv tests, der beskriver den ønskede adfærd, før du skriver implementeringskoden. Dette sikrer, at koden skrives med testbarhed for øje fra starten.
Integrationstest med Jest
Selvom Jest excellerer i enhedstests, kan det også bruges til integrationstests. At mocke færre afhængigheder eller bruge værktøjer som Jests runInBand
-option kan hjælpe.
Eksempel: Test af API-interaktion (forenklet)
// 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 (Integration test)
import axios from 'axios';
import { createProduct } from './apiService';
// Mock axios til integrationstests for at kontrollere netværkslaget
jest.mock('axios');
test('opretter et produkt 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);
});
Parallelisme og konfiguration
Jest kan køre tests parallelt for at fremskynde eksekveringen. Konfigurer dette i din jest.config.js
. For eksempel styrer indstillingen maxWorkers
antallet af parallelle processer.
Dækningsrapporter
Brug Jests indbyggede dækningsrapportering til at identificere dele af din kodebase, der ikke bliver testet. Kør tests med --coverage
for at generere detaljerede rapporter.
jest --coverage
Gennemgang af dækningsrapporter hjælper med at sikre, at dine avancerede testmønstre effektivt dækker kritisk logik, herunder internationaliserings- og lokaliseringskodestier.
Konklusion
At mestre avancerede Jest-testmønstre er et markant skridt mod at bygge pålidelig, vedligeholdelsesvenlig software af høj kvalitet til et globalt publikum. Ved effektivt at bruge mocking, snapshot-test, custom matchers og asynkrone testteknikker kan du forbedre robustheden af din test-suite og opnå større tillid til din applikations adfærd på tværs af forskellige scenarier og regioner. At omfavne disse mønstre styrker udviklingsteams verden over til at levere enestående brugeroplevelser.
Begynd at indarbejde disse avancerede teknikker i din arbejdsgang i dag for at løfte din praksis inden for JavaScript-test.