Українська

Дізнайтеся, як ефективно використовувати мок-функції у вашій стратегії тестування для надійної та стабільної розробки програмного забезпечення. Цей посібник розглядає, коли, чому та як впроваджувати моки з практичними прикладами.

Мок-функції: вичерпний посібник для розробників

У світі розробки програмного забезпечення написання надійного та стабільного коду має першочергове значення. Ретельне тестування є вирішальним для досягнення цієї мети. Модульне тестування, зокрема, зосереджується на тестуванні окремих компонентів або функцій в ізоляції. Однак реальні додатки часто мають складні залежності, що ускладнює тестування модулів у повній ізоляції. Саме тут на допомогу приходять мок-функції.

Що таке мок-функції?

Мок-функція — це імітована версія реальної функції, яку можна використовувати у тестах. Замість виконання логіки реальної функції, мок-функція дозволяє вам контролювати її поведінку, спостерігати, як її викликають, і визначати значення, які вона повертає. Вони є одним із видів тестових двійників.

Уявіть це так: ви тестуєте двигун автомобіля (тестований модуль). Двигун залежить від різних інших компонентів, таких як система впорскування палива та система охолодження. Замість того, щоб запускати справжні системи впорскування палива та охолодження під час тестування двигуна, ви можете використовувати імітаційні системи (моки), які симулюють їхню поведінку. Це дозволяє ізолювати двигун і зосередитися саме на його продуктивності.

Мок-функції є потужними інструментами для:

Коли використовувати мок-функції

Моки є найбільш корисними в таких ситуаціях:

1. Ізоляція модулів із зовнішніми залежностями

Коли ваш тестований модуль залежить від зовнішніх сервісів, баз даних, API або інших компонентів, використання реальних залежностей під час тестування може спричинити кілька проблем:

Приклад: Уявіть, що ви тестуєте функцію, яка отримує дані користувача з віддаленого API. Замість того, щоб робити реальні виклики API під час тестування, ви можете використовувати мок-функцію для імітації відповіді API. Це дозволяє вам тестувати логіку функції, не покладаючись на доступність або продуктивність зовнішнього API. Це особливо важливо, коли API має обмеження на кількість запитів або пов'язані з ними витрати.

2. Тестування складних взаємодій

У деяких випадках ваш тестований модуль може взаємодіяти з іншими компонентами складними способами. Мок-функції дозволяють вам спостерігати та перевіряти ці взаємодії.

Приклад: Розглянемо функцію, яка обробляє платіжні транзакції. Ця функція може взаємодіяти з платіжним шлюзом, базою даних та службою сповіщень. Використовуючи мок-функції, ви можете перевірити, що функція викликає платіжний шлюз із правильними даними транзакції, оновлює базу даних зі статусом транзакції та надсилає сповіщення користувачеві.

3. Симуляція умов помилок

Тестування обробки помилок є вирішальним для забезпечення надійності вашого додатка. Мок-функції дозволяють легко симулювати умови помилок, які важко або неможливо відтворити в реальному середовищі.

Приклад: Припустимо, ви тестуєте функцію, яка завантажує файли до хмарного сховища. Ви можете використовувати мок-функцію для імітації мережевої помилки під час процесу завантаження. Це дозволяє вам перевірити, чи функція правильно обробляє помилку, повторює спробу завантаження або сповіщає користувача.

4. Тестування асинхронного коду

Асинхронний код, такий як код, що використовує колбеки, проміси або async/await, може бути складним для тестування. Мок-функції можуть допомогти вам контролювати час та поведінку асинхронних операцій.

Приклад: Уявіть, що ви тестуєте функцію, яка отримує дані з сервера за допомогою асинхронного запиту. Ви можете використовувати мок-функцію для імітації відповіді сервера та контролювати, коли відповідь буде повернута. Це дозволяє вам тестувати, як функція обробляє різні сценарії відповідей та тайм-аути.

5. Запобігання ненавмисним побічним ефектам

Іноді виклик реальної функції під час тестування може мати ненавмисні побічні ефекти, такі як зміна даних у базі, надсилання електронних листів або запуск зовнішніх процесів. Мок-функції запобігають цим побічним ефектам, дозволяючи вам замінити реальну функцію контрольованою імітацією.

Приклад: Ви тестуєте функцію, яка надсилає вітальні листи новим користувачам. Використовуючи мок-сервіс електронної пошти, ви можете гарантувати, що функція надсилання листів насправді не надсилатиме їх реальним користувачам під час запуску вашого набору тестів. Натомість ви можете перевірити, що функція намагається надіслати лист із правильною інформацією.

Як використовувати мок-функції

Конкретні кроки для використання мок-функцій залежать від мови програмування та фреймворку для тестування, який ви використовуєте. Однак загальний процес зазвичай включає наступні кроки:

  1. Визначення залежностей: Визначте, які зовнішні залежності вам потрібно імітувати.
  2. Створення мок-об'єктів: Створіть мок-об'єкти або функції для заміни реальних залежностей. Ці моки часто матимуть такі властивості, як `called`, `returnValue` та `callArguments`.
  3. Налаштування поведінки моків: Визначте поведінку мок-функцій, наприклад, значення, які вони повертають, умови помилок та кількість викликів.
  4. Впровадження моків: Замініть реальні залежності мок-об'єктами у вашому тестованому модулі. Це часто робиться за допомогою впровадження залежностей.
  5. Виконання тесту: Запустіть ваш тест і спостерігайте, як тестований модуль взаємодіє з мок-функціями.
  6. Перевірка взаємодій: Перевірте, що мок-функції були викликані з очікуваними аргументами, значеннями, що повертаються, та необхідну кількість разів.
  7. Відновлення оригінальної функціональності: Після тесту відновіть оригінальну функціональність, видаливши мок-об'єкти та повернувшись до реальних залежностей. Це допомагає уникнути побічних ефектів для інших тестів.

Приклади мок-функцій різними мовами

Ось приклади використання мок-функцій у популярних мовах програмування та фреймворках для тестування:

JavaScript з Jest

Jest — це популярний фреймворк для тестування JavaScript, який надає вбудовану підтримку для мок-функцій.

// Функція для тестування
function fetchData(callback) {
  setTimeout(() => {
    callback('Data from server');
  }, 100);
}

// Тестовий випадок
test('fetchData викликає колбек з правильними даними', (done) => {
  const mockCallback = jest.fn();
  fetchData(mockCallback);

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

У цьому прикладі `jest.fn()` створює мок-функцію, яка замінює реальну функцію зворотного виклику. Тест перевіряє, що мок-функція викликається з правильними даними за допомогою `toHaveBeenCalledWith()`.

Більш просунутий приклад з використанням модулів:

// 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) {
  // Імітація виклику API
  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('має відображати ім\'я користувача', async () => {
    // Мокуємо функцію getUserDataFromAPI
    const mockGetUserData = jest.spyOn(api, 'getUserDataFromAPI');
    mockGetUserData.mockResolvedValue({ id: 123, name: 'Mocked Name' });

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

    // Відновлюємо оригінальну функцію
    mockGetUserData.mockRestore();
  });
});

Тут `jest.spyOn` використовується для створення мок-функції для `getUserDataFromAPI`, імпортованої з модуля `./api`. `mockResolvedValue` використовується для визначення значення, яке поверне мок. `mockRestore` є важливим для того, щоб інші тести ненавмисно не використовували моковану версію.

Python з pytest та unittest.mock

Python пропонує кілька бібліотек для мокування, зокрема `unittest.mock` (вбудована) та бібліотеки на кшталт `pytest-mock` для спрощеного використання з pytest.

# Функція для тестування
def get_data_from_api(url):
    # У реальному сценарії тут був би виклик API
    # Для простоти імітуємо виклик API
    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"

# Тестовий випадок з використанням unittest.mock
import unittest
from unittest.mock import patch

class TestProcessData(unittest.TestCase):
    @patch('__main__.get_data_from_api') # Замінюємо get_data_from_api у головному модулі
    def test_process_data_success(self, mock_get_data_from_api):
        # Налаштовуємо мок
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # Викликаємо тестовану функцію
        result = process_data("https://example.com/api")

        # Перевіряємо результат
        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()

Цей приклад використовує `unittest.mock.patch` для заміни функції `get_data_from_api` на мок. Тест налаштовує мок для повернення конкретного значення, а потім перевіряє, що функція `process_data` повертає очікуваний результат.

Ось той самий приклад з використанням `pytest-mock`:

# версія pytest
import pytest

def get_data_from_api(url):
    # У реальному сценарії тут був би виклик API
    # Для простоти імітуємо виклик API
    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` надає фікстуру `mocker`, яка спрощує створення та налаштування моків у тестах pytest.

Java з Mockito

Mockito — популярний фреймворк для мокування в 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() {
        // Створюємо мок DataFetcher
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

        // Налаштовуємо мок
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn("API Data");

        // Створюємо DataProcessor з моком
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // Викликаємо тестовану функцію
        String result = dataProcessor.processData("https://example.com/api");

        // Перевіряємо результат
        assertEquals("Processed: API Data", result);

        // Перевіряємо, що мок був викликаний
        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");
    }
}

У цьому прикладі `Mockito.mock()` створює мок-об'єкт для інтерфейсу `DataFetcher`. `when()` використовується для налаштування значення, яке повертає мок, а `verify()` — для перевірки того, що мок був викликаний з очікуваними аргументами.

Найкращі практики використання мок-функцій

Альтернативи мок-функціям

Хоча мок-функції є потужним інструментом, вони не завжди є найкращим рішенням. У деяких випадках інші техніки можуть бути більш доречними:

Висновок

Мок-функції є важливим інструментом для написання ефективних модульних тестів, що дозволяє ізолювати модулі, контролювати поведінку, симулювати умови помилок та тестувати асинхронний код. Дотримуючись найкращих практик та розуміючи альтернативи, ви можете використовувати мок-функції для створення більш надійного, стабільного та легкого в обслуговуванні програмного забезпечення. Не забувайте враховувати компроміси та обирати правильну техніку тестування для кожної ситуації, щоб створити комплексну та ефективну стратегію тестування, незалежно від того, в якій частині світу ви розробляєте.