Svenska

Bemästra avancerade Jest-testmönster för att bygga mer tillförlitlig och underhållbar programvara. Utforska tekniker som mocking, snapshot-testning och mer.

Jest: Avancerade testmönster för robust programvara

I dagens snabbrörliga landskap för programvaruutveckling är det av yttersta vikt att säkerställa tillförlitligheten och stabiliteten i din kodbas. Även om Jest har blivit en de facto-standard för JavaScript-testning, låser man upp en ny nivå av förtroende för sina applikationer genom att gå bortom grundläggande enhetstester. Detta inlägg fördjupar sig i avancerade Jest-testmönster som är avgörande för att bygga robust programvara, riktat till en global publik av utvecklare.

Varför gå bortom grundläggande enhetstester?

Grundläggande enhetstester verifierar enskilda komponenter isolerat. Verkliga applikationer är dock komplexa system där komponenter interagerar. Avancerade testmönster hanterar dessa komplexiteter genom att göra det möjligt för oss att:

Bemästra mocking och spies

Mocking (eller mockning) är avgörande för att isolera den enhet som testas genom att ersätta dess beroenden med kontrollerade substitut. Jest erbjuder kraftfulla verktyg för detta:

jest.fn(): Grunden för mocks och spies

jest.fn() skapar en mock-funktion. Du kan spåra dess anrop, argument och returvärden. Detta är byggstenen för mer sofistikerade mocking-strategier.

Exempel: Spåra funktionsanrop

// component.js
export const fetchData = () => {
  // Simulerar ett API-anrop
  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('ska bearbeta data korrekt', 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(): Observera utan att ersätta

jest.spyOn() låter dig observera anrop till en metod på ett befintligt objekt utan att nödvändigtvis ersätta dess implementation. Du kan också mocka implementationen om det behövs.

Exempel: Spionera på en modulmetod

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

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

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

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

test('ska logga start och slutförande av uppgift', () => {
  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(); // Viktigt att återställa den ursprungliga implementationen
});

Mocka modulimporter

Jests funktioner för att mocka moduler är omfattande. Du kan mocka hela moduler eller specifika exporter.

Exempel: Mocka en extern 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';

// Mocka hela api-modulen
jest.mock('./api');

test('ska hämta fullständigt namn med mockat API', async () => {
  // Mocka den specifika funktionen från den mockade modulen
  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);
});

Automatisk mocking vs. manuell mocking

Jest mockar automatiskt Node.js-moduler. För ES-moduler eller anpassade moduler kan du behöva jest.mock(). För mer kontroll kan du skapa __mocks__-kataloger.

Mock-implementationer

Du kan tillhandahålla anpassade implementationer för dina mocks.

Exempel: Mocka med en anpassad implementation

// 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';

// Mocka hela math-modulen
jest.mock('./math');

test('ska utföra addition med mockad math.add', () => {
  // Tillhandahåll en mock-implementation för 'add'-funktionen
  math.add.mockImplementation((a, b) => a + b + 10); // Lägg till 10 till resultatet
  math.subtract.mockReturnValue(5); // Mocka även 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-testning: Bevara UI och konfiguration

Snapshot-tester är en kraftfull funktion för att fånga resultatet av dina komponenter eller konfigurationer. De är särskilt användbara för UI-testning eller för att verifiera komplexa datastrukturer.

Hur snapshot-testning fungerar

Första gången ett snapshot-test körs skapar Jest en .snap-fil som innehåller en serialiserad representation av det testade värdet. Vid efterföljande körningar jämför Jest det aktuella resultatet med den sparade snapshoten. Om de skiljer sig misslyckas testet, vilket uppmärksammar dig på oavsiktliga ändringar. Detta är ovärderligt för att upptäcka regressioner i UI-komponenter över olika regioner eller språkversioner.

Exempel: Snapshot-test av en React-komponent

Anta att 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 ? 'Aktiv' : 'Inaktiv'}</p>
  </div>
);

export default UserProfile;

// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // För React-komponentsnapshots
import UserProfile from './UserProfile';

test('renderar UserProfile korrekt', () => {
  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('renderar inaktiv UserProfile korrekt', () => {
  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('inaktiv användarprofil'); // Namngiven snapshot
});

Efter att ha kört testerna kommer Jest att skapa en UserProfile.test.js.snap-fil. När du uppdaterar komponenten måste du granska ändringarna och eventuellt uppdatera snapshoten genom att köra Jest med flaggan --updateSnapshot eller -u.

Bästa praxis för snapshot-testning

Anpassade matchers: Förbättra testläsbarheten

Jests inbyggda matchers är omfattande, men ibland behöver du verifiera specifika villkor som inte täcks. Anpassade matchers låter dig skapa din egen assertionslogik, vilket gör dina tester mer uttrycksfulla och läsbara.

Skapa anpassade matchers

Du kan utöka Jests expect-objekt med dina egna matchers.

Exempel: Kontrollera ett giltigt e-postformat

I din Jest-setup-fil (t.ex. jest.setup.js, konfigurerad 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: () => `förväntade att ${received} inte skulle vara en giltig e-postadress`,
        pass: true,
      };
    } else {
      return {
        message: () => `förväntade att ${received} skulle vara en giltig e-postadress`,
        pass: false,
      };
    }
  },
});

// I din jest.config.js
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };

I din testfil:

// validation.test.js

test('ska validera e-postformat', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
  expect('another.test@sub.domain.co.uk').toBeValidEmail();
});

Fördelar med anpassade matchers

Testa asynkrona operationer

JavaScript är i hög grad asynkront. Jest ger utmärkt stöd för att testa promises och async/await.

Använda async/await

Detta är det moderna och mest läsbara sättet att testa asynkron kod.

Exempel: Testa en asynkron funktion

// dataService.js
export const fetchUserData = async (userId) => {
  // Simulera hämtning av data efter en fördröjning
  await new Promise(resolve => setTimeout(resolve, 50));
  if (userId === 1) {
    return { id: 1, name: 'Alice' };
  } else {
    throw new Error('Användaren hittades inte');
  }
};

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

test('hämtar användardata korrekt', async () => {
  const user = await fetchUserData(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

test('kastar ett fel för en obefintlig användare', async () => {
  await expect(fetchUserData(2)).rejects.toThrow('Användaren hittades inte');
});

Använda .resolves och .rejects

Dessa matchers förenklar testning av promise-uppfyllanden och -avslag.

Exempel: Använda .resolves/.rejects

// dataService.test.js (fortsättning)

test('hämtar användardata med .resolves', () => {
  return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('kastar ett fel för en obefintlig användare med .rejects', () => {
  return expect(fetchUserData(2)).rejects.toThrow('Användaren hittades inte');
});

Hantera timers

För funktioner som använder setTimeout eller setInterval erbjuder Jest kontroll över timers.

Exempel: Kontrollera timers

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

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

jest.useFakeTimers(); // Aktivera fejkade timers

test('hälsar efter fördröjning', () => {
  const mockCallback = jest.fn();
  greetAfterDelay('Världen', mockCallback);

  // Flytta fram timers med 1000ms
  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
  expect(mockCallback).toHaveBeenCalledWith('Hej, Världen!');
});

// Återställ riktiga timers om det behövs någon annanstans
jest.useRealTimers();

Testorganisation och struktur

När din testsvit växer blir organisation avgörande för underhållbarheten.

Describe-block och It-block

Använd describe för att gruppera relaterade tester och it (eller test) för enskilda testfall. Denna struktur speglar applikationens modularitet.

Exempel: Strukturerade tester

describe('Användarautentiseringstjänst', () => {
  let authService;

  beforeEach(() => {
    // Sätt upp mocks eller tjänsteinstanser före varje test
    authService = require('./authService');
    jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
  });

  afterEach(() => {
    // Städa upp mocks
    jest.restoreAllMocks();
  });

  describe('inloggningsfunktionalitet', () => {
    it('ska lyckas logga in en användare med giltiga uppgifter', async () => {
      const result = await authService.login('user@example.com', 'password123');
      expect(result.token).toBeDefined();
      // ... fler assertioner ...
    });

    it('ska misslyckas med inloggning med ogiltiga uppgifter', async () => {
      jest.spyOn(authService, 'login').mockRejectedValue(new Error('Ogiltiga uppgifter'));
      await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Ogiltiga uppgifter');
    });
  });

  describe('utloggningsfunktionalitet', () => {
    it('ska rensa användarsessionen', async () => {
      // Testa utloggningslogik...
    });
  });
});

Setup- och Teardown-hooks

Dessa hooks är avgörande för att sätta upp mock-data, databasanslutningar eller städa upp resurser mellan testerna.

Testning för en global publik

När man utvecklar applikationer för en global publik utökas testaspekterna:

Internationalisering (i18n) och lokalisering (l10n)

Säkerställ att ditt UI och dina meddelanden anpassas korrekt till olika språk och regionala format.

Exempel: Testa lokaliserad datumformatering

// 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('formaterar datum korrekt för amerikansk lokal', () => {
  const date = new Date(2023, 10, 15); // 15 november 2023
  expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});

test('formaterar datum korrekt för tysk lokal', () => {
  const date = new Date(2023, 10, 15);
  expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});

Medvetenhet om tidszoner

Testa hur din applikation hanterar olika tidszoner, särskilt för funktioner som schemaläggning eller realtidsuppdateringar. Att mocka systemklockan eller använda bibliotek som abstraherar tidszoner kan vara fördelaktigt.

Kulturella nyanser i data

Tänk på hur siffror, valutor och andra datarepresentationer kan uppfattas eller förväntas olika i olika kulturer. Anpassade matchers kan vara särskilt användbara här.

Avancerade tekniker och strategier

Testdriven utveckling (TDD) och beteendedriven utveckling (BDD)

Jest passar väl ihop med metoderna TDD (Red-Green-Refactor) och BDD (Given-When-Then). Skriv tester som beskriver det önskade beteendet innan du skriver implementationskoden. Detta säkerställer att koden skrivs med testbarhet i åtanke från första början.

Integrationstestning med Jest

Även om Jest utmärker sig på enhetstester kan det också användas för integrationstester. Att mocka färre beroenden eller använda verktyg som Jests runInBand-alternativ kan hjälpa.

Exempel: Testa API-interaktion (förenklat)

// 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 (Integrationstest)
import axios from 'axios';
import { createProduct } from './apiService';

// Mocka axios för integrationstester för att kontrollera nätverkslagret
jest.mock('axios');

test('skapar en 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);
});

Parallellism och konfiguration

Jest kan köra tester parallellt för att påskynda exekveringen. Konfigurera detta i din jest.config.js. Att till exempel ställa in maxWorkers styr antalet parallella processer.

Täckningsrapporter

Använd Jests inbyggda täckningsrapportering för att identifiera delar av din kodbas som inte testas. Kör tester med --coverage för att generera detaljerade rapporter.

jest --coverage

Att granska täckningsrapporter hjälper till att säkerställa att dina avancerade testmönster effektivt täcker kritisk logik, inklusive kodvägar för internationalisering och lokalisering.

Sammanfattning

Att bemästra avancerade Jest-testmönster är ett betydande steg mot att bygga tillförlitlig, underhållbar och högkvalitativ programvara för en global publik. Genom att effektivt använda mocking, snapshot-testning, anpassade matchers och asynkrona testtekniker kan du förbättra robustheten i din testsvit och få större förtroende för din applikations beteende i olika scenarier och regioner. Att anamma dessa mönster ger utvecklingsteam över hela världen möjlighet att leverera exceptionella användarupplevelser.

Börja införliva dessa avancerade tekniker i ditt arbetsflöde idag för att lyfta dina JavaScript-testmetoder.

Jest: Avancerade testmönster för robust programvara | MLOG