Hrvatski

Naučite kako učinkovito koristiti mock funkcije u svojoj strategiji testiranja za robustan i pouzdan razvoj softvera. Vodič pokriva kada, zašto i kako implementirati mockove s praktičnim primjerima.

Mock funkcije: Sveobuhvatan vodič za programere

U svijetu razvoja softvera, pisanje robusnog i pouzdanog koda je od iznimne važnosti. Temeljito testiranje ključno je za postizanje tog cilja. Jedinično testiranje (unit testing) se posebno usredotočuje na testiranje pojedinačnih komponenti ili funkcija u izolaciji. Međutim, stvarne aplikacije često uključuju složene ovisnosti, što otežava testiranje jedinica u potpunoj izolaciji. Tu na scenu stupaju mock funkcije.

Što su mock funkcije?

Mock funkcija je simulirana verzija stvarne funkcije koju možete koristiti u svojim testovima. Umjesto izvršavanja logike stvarne funkcije, mock funkcija vam omogućuje da kontrolirate njezino ponašanje, promatrate kako se poziva i definirate njezine povratne vrijednosti. One su vrsta testnog dvojnika (test double).

Zamislite to ovako: testirate motor automobila (jedinica koja se testira). Motor se oslanja na razne druge komponente, poput sustava za ubrizgavanje goriva i sustava za hlađenje. Umjesto pokretanja stvarnih sustava za ubrizgavanje goriva i hlađenje tijekom testa motora, možete koristiti mock sustave koji simuliraju njihovo ponašanje. To vam omogućuje da izolirate motor i usredotočite se isključivo na njegovu izvedbu.

Mock funkcije su moćni alati za:

Kada koristiti mock funkcije

Mocks su najkorisniji u ovim situacijama:

1. Izoliranje jedinica s vanjskim ovisnostima

Kada jedinica koju testirate ovisi o vanjskim servisima, bazama podataka, API-jima ili drugim komponentama, korištenje stvarnih ovisnosti tijekom testiranja može uzrokovati nekoliko problema:

Primjer: Zamislite da testirate funkciju koja dohvaća korisničke podatke s udaljenog API-ja. Umjesto stvarnih poziva API-ju tijekom testiranja, možete koristiti mock funkciju za simulaciju odgovora API-ja. To vam omogućuje testiranje logike funkcije bez oslanjanja na dostupnost ili performanse vanjskog API-ja. Ovo je posebno važno kada API ima ograničenja poziva (rate limits) ili povezane troškove za svaki zahtjev.

2. Testiranje složenih interakcija

U nekim slučajevima, vaša jedinica koja se testira može komunicirati s drugim komponentama na složene načine. Mock funkcije vam omogućuju promatranje i provjeru tih interakcija.

Primjer: Razmotrite funkciju koja obrađuje platne transakcije. Ova funkcija može komunicirati s pristupnikom za plaćanje (payment gateway), bazom podataka i servisom za obavijesti. Koristeći mock funkcije, možete provjeriti da funkcija poziva pristupnik za plaćanje s točnim detaljima transakcije, ažurira bazu podataka sa statusom transakcije i šalje obavijest korisniku.

3. Simuliranje uvjeta pogreške

Testiranje rukovanja pogreškama ključno je za osiguravanje robusnosti vaše aplikacije. Mock funkcije olakšavaju simulaciju uvjeta pogreške koje je teško ili nemoguće reproducirati u stvarnom okruženju.

Primjer: Pretpostavimo da testirate funkciju koja prenosi datoteke na uslugu pohrane u oblaku. Možete koristiti mock funkciju za simulaciju mrežne pogreške tijekom procesa prijenosa. To vam omogućuje da provjerite da funkcija ispravno rukuje pogreškom, ponovno pokuša prijenos ili obavijesti korisnika.

4. Testiranje asinkronog koda

Asinkroni kod, poput koda koji koristi povratne pozive (callbacks), obećanja (promises) ili async/await, može biti izazovan za testiranje. Mock funkcije mogu vam pomoći u kontroli vremena i ponašanja asinkronih operacija.

Primjer: Zamislite da testirate funkciju koja dohvaća podatke s poslužitelja pomoću asinkronog zahtjeva. Možete koristiti mock funkciju za simulaciju odgovora poslužitelja i kontrolu vremena povratka odgovora. To vam omogućuje da testirate kako funkcija rukuje različitim scenarijima odgovora i vremenskim ograničenjima (timeouts).

5. Sprječavanje neželjenih nuspojava

Ponekad pozivanje stvarne funkcije tijekom testiranja može imati neželjene nuspojave, poput izmjene baze podataka, slanja e-mailova ili pokretanja vanjskih procesa. Mock funkcije sprječavaju te nuspojave omogućujući vam da zamijenite stvarnu funkciju kontroliranom simulacijom.

Primjer: Testirate funkciju koja šalje e-mailove dobrodošlice novim korisnicima. Korištenjem mock servisa za e-poštu, možete osigurati da funkcionalnost slanja e-pošte zapravo ne šalje e-mailove stvarnim korisnicima tijekom izvođenja vašeg testnog paketa. Umjesto toga, možete provjeriti da funkcija pokušava poslati e-mail s točnim informacijama.

Kako koristiti mock funkcije

Specifični koraci za korištenje mock funkcija ovise o programskom jeziku i okviru za testiranje koji koristite. Međutim, opći postupak obično uključuje sljedeće korake:

  1. Identificirajte ovisnosti: Odredite koje vanjske ovisnosti trebate mockirati.
  2. Stvorite mock objekte: Stvorite mock objekte ili funkcije kako biste zamijenili stvarne ovisnosti. Ovi mockovi će često imati svojstva poput `called`, `returnValue` i `callArguments`.
  3. Konfigurirajte ponašanje mocka: Definirajte ponašanje mock funkcija, kao što su njihove povratne vrijednosti, uvjeti pogreške i broj poziva.
  4. Ubacite mockove: Zamijenite stvarne ovisnosti s mock objektima u vašoj jedinici koja se testira. To se često radi pomoću ubacivanja ovisnosti (dependency injection).
  5. Izvršite test: Pokrenite svoj test i promatrajte kako jedinica koja se testira komunicira s mock funkcijama.
  6. Provjerite interakcije: Provjerite jesu li mock funkcije pozvane s očekivanim argumentima, povratnim vrijednostima i brojem poziva.
  7. Vratite izvornu funkcionalnost: Nakon testa, vratite izvornu funkcionalnost uklanjanjem mock objekata i vraćanjem na stvarne ovisnosti. To pomaže izbjeći nuspojave na drugim testovima.

Primjeri mock funkcija u različitim jezicima

Evo primjera korištenja mock funkcija u popularnim programskim jezicima i okvirima za testiranje:

JavaScript s Jestom

Jest je popularan JavaScript okvir za testiranje koji pruža ugrađenu podršku za mock funkcije.

// Function to 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);
});

U ovom primjeru, `jest.fn()` stvara mock funkciju koja zamjenjuje stvarnu callback funkciju. Test provjerava je li mock funkcija pozvana s točnim podacima pomoću `toHaveBeenCalledWith()`.

Napredniji primjer korištenja modula:

// 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();
  });
});

Ovdje se `jest.spyOn` koristi za stvaranje mock funkcije za `getUserDataFromAPI` funkciju uvezenu iz `./api` modula. `mockResolvedValue` se koristi za specificiranje povratne vrijednosti mocka. `mockRestore` je ključan kako bi se osiguralo da drugi testovi nehotice ne koriste mockiranu verziju.

Python s pytest i unittest.mock

Python nudi nekoliko biblioteka za mockiranje, uključujući `unittest.mock` (ugrađenu) i biblioteke poput `pytest-mock` za pojednostavljenu upotrebu s pytestom.

# Function to test
def get_data_from_api(url):
    # In a real scenario, this would make an API call
    # For simplicity, we simulate an API call
    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 using unittest.mock
import unittest
from unittest.mock import patch

class TestProcessData(unittest.TestCase):
    @patch('__main__.get_data_from_api') # Replace get_data_from_api in the main module
    def test_process_data_success(self, mock_get_data_from_api):
        # Configure the mock
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # Call the function being tested
        result = process_data("https://example.com/api")

        # Assert the result
        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()

Ovaj primjer koristi `unittest.mock.patch` za zamjenu `get_data_from_api` funkcije mockom. Test konfigurira mock da vrati specifičnu vrijednost, a zatim provjerava da funkcija `process_data` vraća očekivani rezultat.

Evo istog primjera koristeći `pytest-mock`:

# pytest version
import pytest

def get_data_from_api(url):
    # In a real scenario, this would make an API call
    # For simplicity, we simulate an API call
    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` biblioteka pruža `mocker` fixture koji pojednostavljuje stvaranje i konfiguraciju mockova unutar pytest testova.

Java s Mockitom

Mockito je popularan okvir za mockiranje u Javi.

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() {
        // Create a mock DataFetcher
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

        // Configure the mock
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn("API Data");

        // Create the DataProcessor with the mock
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // Call the function being tested
        String result = dataProcessor.processData("https://example.com/api");

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

        // Verify that the mock was called
        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");
    }
}

U ovom primjeru, `Mockito.mock()` stvara mock objekt za `DataFetcher` sučelje. `when()` se koristi za konfiguriranje povratne vrijednosti mocka, a `verify()` se koristi za provjeru je li mock pozvan s očekivanim argumentima.

Najbolje prakse za korištenje mock funkcija

Alternative mock funkcijama

Iako su mock funkcije moćan alat, nisu uvijek najbolje rješenje. U nekim slučajevima, druge tehnike mogu biti prikladnije:

Zaključak

Mock funkcije su ključan alat za pisanje učinkovitih jediničnih testova, omogućujući vam da izolirate jedinice, kontrolirate ponašanje, simulirate uvjete pogreške i testirate asinkroni kod. Slijedeći najbolje prakse i razumijevajući alternative, možete iskoristiti mock funkcije za izgradnju robusnijeg, pouzdanijeg i lakše održivog softvera. Ne zaboravite razmotriti kompromise i odabrati pravu tehniku testiranja za svaku situaciju kako biste stvorili sveobuhvatnu i učinkovitu strategiju testiranja, bez obzira iz kojeg dijela svijeta gradite.