Dansk

Lær at bruge mock-funktioner effektivt i din teststrategi for robust og pålidelig softwareudvikling. Denne guide dækker hvornår, hvorfor og hvordan du implementerer mocks med praktiske eksempler.

Mock-funktioner: En Omfattende Guide for Udviklere

I softwareudviklingens verden er det altafgørende at skrive robust og pålidelig kode. Grundig testning er essentiel for at opnå dette mål. Enhedstest, især, fokuserer på at teste individuelle komponenter eller funktioner isoleret. Dog involverer virkelige applikationer ofte komplekse afhængigheder, hvilket gør det udfordrende at teste enheder i fuldstændig isolation. Det er her, mock-funktioner kommer ind i billedet.

Hvad er Mock-funktioner?

En mock-funktion er en simuleret version af en rigtig funktion, som du kan bruge i dine tests. I stedet for at eksekvere den faktiske funktions logik, giver en mock-funktion dig mulighed for at kontrollere dens adfærd, observere hvordan den bliver kaldt og definere dens returværdier. De er en type test double.

Tænk på det sådan her: Forestil dig, at du tester en bils motor (enheden under test). Motoren er afhængig af forskellige andre komponenter, som brændstofindsprøjtningssystemet og kølesystemet. I stedet for at køre de faktiske brændstofindsprøjtnings- og kølesystemer under motortesten, kan du bruge mock-systemer, der simulerer deres adfærd. Dette giver dig mulighed for at isolere motoren og fokusere specifikt på dens ydeevne.

Mock-funktioner er effektive værktøjer til at:

Hvornår skal man bruge Mock-funktioner?

Mocks er mest nyttige i disse situationer:

1. Isolering af enheder med eksterne afhængigheder

Når din enhed under test afhænger af eksterne tjenester, databaser, API'er eller andre komponenter, kan brugen af reelle afhængigheder under testning introducere flere problemer:

Eksempel: Forestil dig, at du tester en funktion, der henter brugerdata fra en ekstern API. I stedet for at foretage faktiske API-kald under testningen, kan du bruge en mock-funktion til at simulere API-svaret. Dette giver dig mulighed for at teste funktionens logik uden at være afhængig af den eksterne API's tilgængelighed eller ydeevne. Dette er især vigtigt, når API'en har rate limits eller omkostninger forbundet med hver anmodning.

2. Test af komplekse interaktioner

I nogle tilfælde kan din enhed under test interagere med andre komponenter på komplekse måder. Mock-funktioner giver dig mulighed for at observere og verificere disse interaktioner.

Eksempel: Overvej en funktion, der behandler betalingstransaktioner. Denne funktion kan interagere med en betalingsgateway, en database og en notifikationstjeneste. Ved hjælp af mock-funktioner kan du verificere, at funktionen kalder betalingsgatewayen med de korrekte transaktionsdetaljer, opdaterer databasen med transaktionsstatus og sender en notifikation til brugeren.

3. Simulering af fejltilstande

Test af fejlhåndtering er afgørende for at sikre robustheden af din applikation. Mock-funktioner gør det nemt at simulere fejltilstande, der er svære eller umulige at reproducere i et virkeligt miljø.

Eksempel: Antag, at du tester en funktion, der uploader filer til en cloud-lagringstjeneste. Du kan bruge en mock-funktion til at simulere en netværksfejl under upload-processen. Dette giver dig mulighed for at verificere, at funktionen håndterer fejlen korrekt, forsøger at uploade igen eller underretter brugeren.

4. Test af asynkron kode

Asynkron kode, såsom kode der bruger callbacks, promises eller async/await, kan være udfordrende at teste. Mock-funktioner kan hjælpe dig med at kontrollere timingen og adfærden af asynkrone operationer.

Eksempel: Forestil dig, at du tester en funktion, der henter data fra en server ved hjælp af en asynkron anmodning. Du kan bruge en mock-funktion til at simulere server-svaret og kontrollere, hvornår svaret returneres. Dette giver dig mulighed for at teste, hvordan funktionen håndterer forskellige svarscenarier og timeouts.

5. Forebyggelse af utilsigtede sideeffekter

Nogle gange kan kald af en rigtig funktion under test have utilsigtede sideeffekter, såsom at ændre en database, sende e-mails eller udløse eksterne processer. Mock-funktioner forhindrer disse sideeffekter ved at lade dig erstatte den rigtige funktion med en kontrolleret simulering.

Eksempel: Du tester en funktion, der sender velkomstmails til nye brugere. Ved at bruge en mock-e-mail-tjeneste kan du sikre, at e-mail-afsendelsesfunktionaliteten ikke rent faktisk sender e-mails til rigtige brugere, mens din test suite kører. I stedet kan du verificere, at funktionen forsøger at sende e-mailen med de korrekte oplysninger.

Sådan bruger du Mock-funktioner

De specifikke trin til at bruge mock-funktioner afhænger af programmeringssproget og det test-framework, du bruger. Dog involverer den generelle proces typisk følgende trin:

  1. Identificer afhængigheder: Find ud af, hvilke eksterne afhængigheder du skal mocke.
  2. Opret mock-objekter: Opret mock-objekter eller funktioner til at erstatte de reelle afhængigheder. Disse mocks vil ofte have egenskaber som `called`, `returnValue`, og `callArguments`.
  3. Konfigurer mock-adfærd: Definer adfærden for mock-funktionerne, såsom deres returværdier, fejltilstande og antal kald.
  4. Injicer mocks: Erstat de reelle afhængigheder med mock-objekterne i din enhed under test. Dette gøres ofte ved hjælp af dependency injection.
  5. Eksekver test: Kør din test og observer, hvordan enheden under test interagerer med mock-funktionerne.
  6. Verificer interaktioner: Verificer, at mock-funktionerne blev kaldt med de forventede argumenter, returværdier og antal gange.
  7. Gendan oprindelig funktionalitet: Efter testen skal du gendanne den oprindelige funktionalitet ved at fjerne mock-objekterne og vende tilbage til de reelle afhængigheder. Dette hjælper med at undgå sideeffekter på andre tests.

Eksempler på Mock-funktioner i forskellige sprog

Her er eksempler på brug af mock-funktioner i populære programmeringssprog og test-frameworks:

JavaScript med Jest

Jest er et populært JavaScript test-framework, der har indbygget understøttelse af mock-funktioner.

// Funktion til test
function fetchData(callback) {
  setTimeout(() => {
    callback('Data from server');
  }, 100);
}

// Test case
test('fetchData calls callback with correct data', (done) => {
  const mockCallback = jest.fn();
  fetchData(mockCallback);

  setTimeout(() => {
    expect(mockCallback).toHaveBeenCalledWith('Data from server');
    done();
  }, 200);
});

I dette eksempel opretter `jest.fn()` en mock-funktion, der erstatter den reelle callback-funktion. Testen verificerer, at mock-funktionen kaldes med de korrekte data ved hjælp af `toHaveBeenCalledWith()`.

Mere avanceret eksempel med moduler:

// user.js
import { getUserDataFromAPI } from './api';

export async function displayUserName(userId) {
  const userData = await getUserDataFromAPI(userId);
  return userData.name;
}

// api.js
export async function getUserDataFromAPI(userId) {
  // Simulate API call
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: userId, name: 'John Doe' });
    }, 50);
  });
}

// user.test.js
import { displayUserName } from './user';
import * as api from './api';

describe('displayUserName', () => {
  it('should display the user name', async () => {
    // Mock the getUserDataFromAPI function
    const mockGetUserData = jest.spyOn(api, 'getUserDataFromAPI');
    mockGetUserData.mockResolvedValue({ id: 123, name: 'Mocked Name' });

    const userName = await displayUserName(123);
    expect(userName).toBe('Mocked Name');

    // Restore the original function
    mockGetUserData.mockRestore();
  });
});

Her bruges `jest.spyOn` til at oprette en mock-funktion for `getUserDataFromAPI`-funktionen, der importeres fra `./api`-modulet. `mockResolvedValue` bruges til at specificere returværdien for mocken. `mockRestore` er essentiel for at sikre, at andre tests ikke utilsigtet bruger den mockede version.

Python med pytest og unittest.mock

Python tilbyder flere biblioteker til mocking, herunder `unittest.mock` (indbygget) og biblioteker som `pytest-mock` for forenklet brug med pytest.

# Funktion til test
def get_data_from_api(url):
    # I et virkeligt scenarie ville dette foretage et API-kald
    # For simpelhedens skyld simulerer vi et API-kald
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"

# Test case med unittest.mock
import unittest
from unittest.mock import patch

class TestProcessData(unittest.TestCase):
    @patch('__main__.get_data_from_api') # Erstat get_data_from_api i hovedmodulet
    def test_process_data_success(self, mock_get_data_from_api):
        # Konfigurer mocken
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # Kald den funktion, der testes
        result = process_data("https://example.com/api")

        # Assert resultatet
        self.assertEqual(result, "Mocked data")
        mock_get_data_from_api.assert_called_once_with("https://example.com/api")

    @patch('__main__.get_data_from_api')
    def test_process_data_failure(self, mock_get_data_from_api):
        mock_get_data_from_api.return_value = None
        result = process_data("https://example.com/api")
        self.assertEqual(result, "No data found")

if __name__ == '__main__':
    unittest.main()

Dette eksempel bruger `unittest.mock.patch` til at erstatte `get_data_from_api`-funktionen med en mock. Testen konfigurerer mocken til at returnere en specifik værdi og verificerer derefter, at `process_data`-funktionen returnerer det forventede resultat.

Her er det samme eksempel med `pytest-mock`:

# pytest version
import pytest

def get_data_from_api(url):
    # I et virkeligt scenarie ville dette foretage et API-kald
    # For simpelhedens skyld simulerer vi et API-kald
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"


def test_process_data_success(mocker):
    mocker.patch('__main__.get_data_from_api', return_value={"data": "Mocked data"})
    result = process_data("https://example.com/api")
    assert result == "Mocked data"


def test_process_data_failure(mocker):
    mocker.patch('__main__.get_data_from_api', return_value=None)
    result = process_data("https://example.com/api")
    assert result == "No data found"

`pytest-mock`-biblioteket tilbyder en `mocker`-fixture, der forenkler oprettelsen og konfigurationen af mocks i pytest-tests.

Java med Mockito

Mockito er et populært mocking-framework til Java.

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

interface DataFetcher {
    String fetchData(String url);
}

class DataProcessor {
    private final DataFetcher dataFetcher;

    public DataProcessor(DataFetcher dataFetcher) {
        this.dataFetcher = dataFetcher;
    }

    public String processData(String url) {
        String data = dataFetcher.fetchData(url);
        if (data != null) {
            return "Processed: " + data;
        } else {
            return "No data";
        }
    }
}

public class DataProcessorTest {

    @Test
    public void testProcessDataSuccess() {
        // Opret en mock DataFetcher
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

        // Konfigurer mocken
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn("API Data");

        // Opret DataProcessor med mocken
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // Kald den funktion, der testes
        String result = dataProcessor.processData("https://example.com/api");

        // Assert resultatet
        assertEquals("Processed: API Data", result);

        // Verificer, at mocken blev kaldt
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }

    @Test
    public void testProcessDataFailure() {
        DataFetcher mockDataFetcher = mock(DataFetcher.class);
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn(null);

        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);
        String result = dataProcessor.processData("https://example.com/api");
        assertEquals("No data", result);
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }
}

I dette eksempel opretter `Mockito.mock()` et mock-objekt for `DataFetcher`-interfacet. `when()` bruges til at konfigurere mockens returværdi, og `verify()` bruges til at verificere, at mocken blev kaldt med de forventede argumenter.

Bedste praksis for brug af Mock-funktioner

Alternativer til Mock-funktioner

Selvom mock-funktioner er et stærkt værktøj, er de ikke altid den bedste løsning. I nogle tilfælde kan andre teknikker være mere passende:

Konklusion

Mock-funktioner er et essentielt værktøj til at skrive effektive enhedstests, der giver dig mulighed for at isolere enheder, kontrollere adfærd, simulere fejltilstande og teste asynkron kode. Ved at følge bedste praksis og forstå alternativerne kan du udnytte mock-funktioner til at bygge mere robust, pålidelig og vedligeholdelsesvenlig software. Husk at overveje kompromiserne og vælge den rigtige testteknik til hver situation for at skabe en omfattende og effektiv teststrategi, uanset hvor i verden du bygger fra.