Norsk

Lær hvordan du effektivt bruker mokk-funksjoner i teststrategien din for robust og pålitelig programvareutvikling. Denne guiden dekker når, hvorfor og hvordan du implementerer mokker med praktiske eksempler.

Mokk-funksjoner: En omfattende guide for utviklere

I en verden av programvareutvikling er det avgjørende å skrive robust og pålitelig kode. Grundig testing er essensielt for å oppnå dette målet. Enhetstesting, spesielt, fokuserer på å teste individuelle komponenter eller funksjoner isolert. Imidlertid involverer virkelige applikasjoner ofte komplekse avhengigheter, noe som gjør det utfordrende å teste enheter i fullstendig isolasjon. Det er her mokk-funksjoner kommer inn i bildet.

Hva er mokk-funksjoner?

En mokk-funksjon er en simulert versjon av en ekte funksjon som du kan bruke i testene dine. I stedet for å utføre den faktiske funksjonens logikk, lar en mokk-funksjon deg kontrollere dens oppførsel, observere hvordan den blir kalt, og definere dens returverdier. De er en type test double.

Tenk på det slik: forestill deg at du tester en bils motor (enheten som testes). Motoren er avhengig av forskjellige andre komponenter, som drivstoffinnsprøytningssystemet og kjølesystemet. I stedet for å kjøre de faktiske drivstoffinnsprøytnings- og kjølesystemene under motortesten, kan du bruke mokk-systemer som simulerer deres oppførsel. Dette lar deg isolere motoren og fokusere spesifikt på dens ytelse.

Mokk-funksjoner er kraftige verktøy for:

Når bør man bruke mokk-funksjoner?

Mokker er mest nyttige i disse situasjonene:

1. Isolere enheter med eksterne avhengigheter

Når enheten du tester avhenger av eksterne tjenester, databaser, API-er eller andre komponenter, kan bruk av ekte avhengigheter under testing introdusere flere problemer:

Eksempel: Tenk deg at du tester en funksjon som henter brukerdata fra et eksternt API. I stedet for å gjøre faktiske API-kall under testing, kan du bruke en mokk-funksjon for å simulere API-responsen. Dette lar deg teste funksjonens logikk uten å være avhengig av tilgjengeligheten eller ytelsen til det eksterne API-et. Dette er spesielt viktig når API-et har rate limits eller tilknyttede kostnader for hver forespørsel.

2. Teste komplekse interaksjoner

I noen tilfeller kan enheten du tester interagere med andre komponenter på komplekse måter. Mokk-funksjoner lar deg observere og verifisere disse interaksjonene.

Eksempel: Vurder en funksjon som behandler betalingstransaksjoner. Denne funksjonen kan interagere med en betalingsgateway, en database og en varslingstjeneste. Ved hjelp av mokk-funksjoner kan du verifisere at funksjonen kaller betalingsgatewayen med de riktige transaksjonsdetaljene, oppdaterer databasen med transaksjonsstatusen og sender en varsling til brukeren.

3. Simulere feiltilstander

Testing av feilhåndtering er avgjørende for å sikre robustheten til applikasjonen din. Mokk-funksjoner gjør det enkelt å simulere feiltilstander som er vanskelige eller umulige å reprodusere i et ekte miljø.

Eksempel: Anta at du tester en funksjon som laster opp filer til en skylagringstjeneste. Du kan bruke en mokk-funksjon for å simulere en nettverksfeil under opplastingsprosessen. Dette lar deg verifisere at funksjonen håndterer feilen korrekt, prøver opplastingen på nytt eller varsler brukeren.

4. Teste asynkron kode

Asynkron kode, som kode som bruker callbacks, promises eller async/await, kan være utfordrende å teste. Mokk-funksjoner kan hjelpe deg med å kontrollere timingen og oppførselen til asynkrone operasjoner.

Eksempel: Tenk deg at du tester en funksjon som henter data fra en server ved hjelp av en asynkron forespørsel. Du kan bruke en mokk-funksjon for å simulere serverresponsen og kontrollere når responsen returneres. Dette lar deg teste hvordan funksjonen håndterer forskjellige responsscenarier og tidsavbrudd.

5. Forhindre utilsiktede sideeffekter

Noen ganger kan det å kalle en ekte funksjon under testing ha utilsiktede sideeffekter, som å endre en database, sende e-poster eller utløse eksterne prosesser. Mokk-funksjoner forhindrer disse sideeffektene ved å la deg erstatte den ekte funksjonen med en kontrollert simulering.

Eksempel: Du tester en funksjon som sender velkomst-e-poster til nye brukere. Ved å bruke en mokk-e-posttjeneste kan du sikre at e-postsendingsfunksjonaliteten ikke faktisk sender e-poster til ekte brukere under kjøringen av testsuiten din. I stedet kan du verifisere at funksjonen forsøker å sende e-posten med riktig informasjon.

Hvordan bruke mokk-funksjoner

De spesifikke trinnene for å bruke mokk-funksjoner avhenger av programmeringsspråket og testrammeverket du bruker. Imidlertid involverer den generelle prosessen vanligvis følgende trinn:

  1. Identifiser avhengigheter: Bestem hvilke eksterne avhengigheter du trenger å mokke.
  2. Opprett mokk-objekter: Lag mokk-objekter eller -funksjoner for å erstatte de ekte avhengighetene. Disse mokkene vil ofte ha egenskaper som `called`, `returnValue`, og `callArguments`.
  3. Konfigurer mokk-oppførsel: Definer oppførselen til mokk-funksjonene, som deres returverdier, feiltilstander og antall kall.
  4. Injiser mokker: Erstatt de ekte avhengighetene med mokk-objektene i enheten du tester. Dette gjøres ofte ved hjelp av dependency injection.
  5. Utfør test: Kjør testen din og observer hvordan enheten som testes interagerer med mokk-funksjonene.
  6. Verifiser interaksjoner: Verifiser at mokk-funksjonene ble kalt med de forventede argumentene, returverdiene og antall ganger.
  7. Gjenopprett original funksjonalitet: Etter testen, gjenopprett den originale funksjonaliteten ved å fjerne mokk-objektene og gå tilbake til de ekte avhengighetene. Dette hjelper med å unngå sideeffekter på andre tester.

Eksempler på mokk-funksjoner i ulike språk

Her er eksempler på bruk av mokk-funksjoner i populære programmeringsspråk og testrammeverk:

JavaScript med Jest

Jest er et populært JavaScript-testrammeverk som har innebygd støtte for mokk-funksjoner.

// Funksjon som skal testes
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 eksempelet lager `jest.fn()` en mokk-funksjon som erstatter den ekte callback-funksjonen. Testen verifiserer at mokk-funksjonen blir kalt med riktig data ved hjelp av `toHaveBeenCalledWith()`.

Mer avansert 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) {
  // Simuler API-kall
  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 () => {
    // Mokk getUserDataFromAPI-funksjonen
    const mockGetUserData = jest.spyOn(api, 'getUserDataFromAPI');
    mockGetUserData.mockResolvedValue({ id: 123, name: 'Mocked Name' });

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

    // Gjenopprett den originale funksjonen
    mockGetUserData.mockRestore();
  });
});

Her brukes `jest.spyOn` til å lage en mokk-funksjon for `getUserDataFromAPI`-funksjonen importert fra `./api`-modulen. `mockResolvedValue` brukes til å spesifisere returverdien til mokken. `mockRestore` er essensielt for å sikre at andre tester ikke utilsiktet bruker den mokkede versjonen.

Python med pytest og unittest.mock

Python tilbyr flere biblioteker for mokking, inkludert `unittest.mock` (innebygd) og biblioteker som `pytest-mock` for forenklet bruk med pytest.

# Funksjon som skal testes
def get_data_from_api(url):
    # I et reelt scenario ville dette gjort et API-kall
    # For enkelhets skyld simulerer vi et API-kall
    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') # Erstatt get_data_from_api i hovedmodulen
    def test_process_data_success(self, mock_get_data_from_api):
        # Konfigurer mokken
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # Kall funksjonen som testes
        result = process_data("https://example.com/api")

        # Verifiser 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 eksempelet bruker `unittest.mock.patch` for å erstatte `get_data_from_api`-funksjonen med en mokk. Testen konfigurerer mokken til å returnere en spesifikk verdi og verifiserer deretter at `process_data`-funksjonen returnerer det forventede resultatet.

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

# pytest-versjon
import pytest

def get_data_from_api(url):
    # I et reelt scenario ville dette gjort et API-kall
    # For enkelhets skyld simulerer vi et API-kall
    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 tilbyr en `mocker`-fixture som forenkler opprettelsen og konfigurasjonen av mokker i pytest-tester.

Java med Mockito

Mockito er et populært mokking-rammeverk for 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() {
        // Opprett en mokk-DataFetcher
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

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

        // Opprett DataProcessor med mokken
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // Kall funksjonen som testes
        String result = dataProcessor.processData("https://example.com/api");

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

        // Verifiser at mokken ble kalt
        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 eksempelet lager `Mockito.mock()` et mokk-objekt for `DataFetcher`-interfacet. `when()` brukes for å konfigurere mokkens returverdi, og `verify()` brukes for å verifisere at mokken ble kalt med de forventede argumentene.

Beste praksis for bruk av mokk-funksjoner

Alternativer til mokk-funksjoner

Selv om mokk-funksjoner er et kraftig verktøy, er de ikke alltid den beste løsningen. I noen tilfeller kan andre teknikker være mer passende:

Konklusjon

Mokk-funksjoner er et essensielt verktøy for å skrive effektive enhetstester, som gjør det mulig å isolere enheter, kontrollere oppførsel, simulere feiltilstander og teste asynkron kode. Ved å følge beste praksis og forstå alternativene, kan du utnytte mokk-funksjoner til å bygge mer robust, pålitelig og vedlikeholdbar programvare. Husk å vurdere avveiningene og velge riktig testteknikk for hver situasjon for å lage en omfattende og effektiv teststrategi, uansett hvor i verden du bygger fra.