mockall для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

mockall для Rust: полное руководство с примерами

1. Что это и зачем нужна

Библиотека mockall — это мощный фреймворк для создания автоматических мок-объектов (mock objects) в языке Rust. Она предназначена для модульного тестирования и позволяет разработчикам легко изолировать тестируемый код от внешних зависимостей, таких как базы данных, HTTP-клиенты, файловые системы или любые другие сервисы. В отличие от ручного написания тестовых заглушек, mockall автоматически генерирует структуры-моки на основе существующих трейтов (traits), предоставляя гибкий API для настройки ожиданий (expectations) и проверки вызовов.

Зачем это нужно? В реальных проектах код редко существует в вакууме. Функции вызывают другие функции, модули зависят от внешних библиотек. При тестировании отдельного модуля мы не хотим, чтобы тест зависел от работоспособности базы данных или сети. Mockall решает эту проблему, позволяя создавать моки, которые имитируют поведение реальных зависимостей. Вы можете задать, что должен вернуть метод при определённых аргументах, сколько раз он должен быть вызван, и даже проверить порядок вызовов. Это делает тесты быстрыми, надёжными и воспроизводимыми.

Ключевая особенность mockall — это интеграция с системой типов Rust и работа на этапе компиляции через атрибут #[automock]. Это означает, что моки генерируются автоматически для любого трейта, что экономит массу времени и уменьшает количество шаблонного кода. Библиотека поддерживает асинхронные функции, дженерики, трейты с ассоциированными типами и многое другое, что делает её незаменимым инструментом для серьёзной разработки на Rust.

2. Установка

Для использования mockall в вашем проекте на Rust, добавьте зависимость в файл Cargo.toml. Библиотека устанавливается через стандартный менеджер пакетов Cargo. Рекомендуется добавлять её только как dev-dependency, так как она используется исключительно для тестирования.


# Cargo.toml
[dev-dependencies]
mockall = "0.13"  # Используйте актуальную версию на момент чтения
        

После добавления строки выполните команду cargo build или cargo test, чтобы Cargo загрузил и скомпилировал библиотеку. Если вы работаете в проекте, где уже есть другие зависимости, убедитесь, что версия mockall совместима с вашей версией Rust (обычно требуется Rust 1.70+). Для проверки актуальной версии посетите crates.io/crates/mockall.

3. Быстрый старт

Давайте создадим минимальный рабочий пример. Предположим, у нас есть трейт Greeter, который мы хотим замокать в тесте. С помощью #[automock] мы автоматически получаем структуру MockGreeter.


// src/lib.rs
use mockall::automock;

#[automock]
pub trait Greeter {
    fn greet(&self, name: &str) -> String;
}

pub fn run_greeter(greeter: &impl Greeter, name: &str) -> String {
    greeter.greet(name)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_greeting() {
        // Создаём мок
        let mut mock = MockGreeter::new();

        // Настраиваем ожидание: при вызове greet("Alice") вернуть "Hello, Alice!"
        mock.expect_greet()
            .with(predicate::eq("Alice"))
            .returning(|name| format!("Hello, {}!", name));

        // Вызываем тестируемую функцию
        let result = run_greeter(&mock, "Alice");

        // Проверяем результат
        assert_eq!(result, "Hello, Alice!");
    }
}
        

Запустите тест командой cargo test. Если всё настроено правильно, тест пройдёт успешно. Обратите внимание: with(predicate::eq("Alice")) задаёт фильтр аргументов, а returning определяет, что вернёт мок. Если вызвать greet с другим именем, тест упадёт с паникой, так как ожидание не будет выполнено.

4. Основные методы и функции

Ниже представлены ключевые методы библиотеки mockall. Для каждого приведена сигнатура, описание и пример использования.

4.1. mockall::automock

Сигнатура: #[automock] (атрибут над трейтом)

Что делает: Генерирует структуру-мок с именем Mock<TraitName> для указанного трейта. Все методы трейта автоматически получают соответствующие методы ожидания (expect_*).


use mockall::automock;

#[automock]
pub trait Calculator {
    fn add(&self, a: i32, b: i32) -> i32;
}

// Теперь доступна структура MockCalculator
let mut mock = MockCalculator::new();
mock.expect_add()
    .with(predicate::eq(2), predicate::eq(3))
    .returning(|a, b| a + b);
assert_eq!(mock.add(2, 3), 5);
            

4.2. Mock::new()

Сигнатура: fn new() -> Self

Что делает: Создаёт новый экземпляр мока. Все методы по умолчанию не имеют ожиданий, и их вызов приведёт к панике.


let mut mock = MockGreeter::new(); // Создаём пустой мок
// mock.greet("test"); // Это вызовет панику! Нужно сначала задать ожидание.
            

4.3. expect_*()

Сигнатура: fn expect_<method_name>(&mut self) -> Expectation

Что делает: Возвращает объект Expectation, который позволяет настроить поведение метода: задать аргументы, возвращаемое значение, количество вызовов и т.д.


let mut mock = MockCalculator::new();
// Настраиваем ожидание для метода add
let expectation = mock.expect_add();
expectation
    .with(predicate::always(), predicate::always())
    .returning(|a, b| a * b); // Переопределяем поведение
            

4.4. with()

Сигнатура: fn with(self, ...predicates) -> Self

Что делает: Задаёт предикаты (условия) для аргументов метода. Если аргументы не соответствуют предикатам, ожидание не срабатывает, и мок ищет другое подходящее ожидание или паникует.


use mockall::predicate;

let mut mock = MockGreeter::new();
mock.expect_greet()
    .with(predicate::eq("Alice")) // Только для "Alice"
    .returning(|_| "Hi, Alice!".to_string());

mock.expect_greet()
    .with(predicate::always()) // Для всех остальных
    .returning(|name| format!("Hello, {}!", name));

assert_eq!(mock.greet("Alice"), "Hi, Alice!");
assert_eq!(mock.greet("Bob"), "Hello, Bob!");
            

4.5. returning()

Сигнатура: fn returning(self, f: impl FnOnce(...) -> R) -> Self

Что делает: Задаёт функцию, которая будет вызвана при активации ожидания. Функция получает аргументы метода и должна вернуть значение соответствующего типа.


let mut mock = MockCalculator::new();
mock.expect_add()
    .with(predicate::eq(10), predicate::eq(20))
    .returning(|a, b| {
        println!("Mock called with {} and {}", a, b);
        a + b + 100 // Всегда добавляем 100
    });
assert_eq!(mock.add(10, 20), 130);
            

4.6. return_const()

Сигнатура: fn return_const(self, value: R) -> Self

Что делает: Упрощённая версия returning, которая всегда возвращает одно и то же значение, независимо от аргументов. Полезна для константных ответов.


let mut mock = MockGreeter::new();
mock.expect_greet()
    .return_const("Default greeting".to_string());

assert_eq!(mock.greet("Alice"), "Default greeting");
assert_eq!(mock.greet("Bob"), "Default greeting");
            

4.7. times()

Сигнатура: fn times(self, count: usize) -> Self

Что делает: Ограничивает количество вызовов, для которых действительно данное ожидание. После исчерпания лимита ожидание перестаёт работать, и мок будет искать другое.


let mut mock = MockGreeter::new();
mock.expect_greet()
    .times(2) // Ожидание сработает только два раза
    .return_const("Hello!".to_string());

assert_eq!(mock.greet("A"), "Hello!");
assert_eq!(mock.greet("B"), "Hello!");
// Третий вызов упадёт с паникой, так как ожидание исчерпано
// mock.greet("C"); // Panic!
            

4.8. predicate::eq()

Сигнатура: pub fn eq(value: T) -> T

Что делает: Создаёт предикат, который проверяет равенство аргумента заданному значению с использованием PartialEq. Это наиболее часто используемый предикат.


use mockall::predicate;

let mut mock = MockCalculator::new();
mock.expect_add()
    .with(predicate::eq(5), predicate::eq(3))
    .returning(|a, b| a - b); // 5 - 3 = 2
assert_eq!(mock.add(5, 3), 2);
            

4.9. predicate::always()

Сигнатура: pub fn always() -> T

Что делает: Создаёт предикат, который принимает любое значение. Используется как "запасной" вариант, когда нужно перехватить все вызовы, не подходящие под более специфичные предикаты.


use mockall::predicate;

let mut mock = MockGreeter::new();
mock.expect_greet()
    .with(predicate::always())
    .returning(|name| format!("Hi, {}!", name));
// Работает для любого имени
assert_eq!(mock.greet("World"), "Hi, World!");
            

4.10. checkpoint()

Сигнатура: fn checkpoint(&mut self)

Что делает: Сбрасывает все текущие ожидания и счётчики вызовов. Полезно, когда нужно переиспользовать мок в нескольких тестах или после проверки одних ожиданий перейти к другим.


let mut mock = MockGreeter::new();

mock.expect_greet()
    .with(predicate::eq("First"))
    .return_const("First call".to_string());
mock.greet("First");

mock.checkpoint(); // Сбрасываем все ожидания

// Теперь можно настроить новые ожидания
mock.expect_greet()
    .with(predicate::eq("Second"))
    .return_const("Second call".to_string());
mock.greet("Second");
            

5. Реальный практический пример

Рассмотрим более сложный сценарий: у нас есть сервис для работы с пользователями, который зависит от внешнего HTTP-клиента. Мы хотим протестировать логику сервиса, не отправляя реальных HTTP-запросов.


// src/lib.rs
use mockall::automock;
use std::error::Error;

// Определяем трейт для HTTP-клиента
#[automock]
pub trait HttpClient {
    fn get(&self, url: &str) -> Result>;
}

// Сервис для работы с пользователями
pub struct UserService {
    client: Box,
}

impl UserService {
    pub fn new(client: Box) -> Self {
        UserService { client }
    }

    // Метод для получения имени пользователя по ID
    pub fn get_user_name(&self, user_id: u32) -> Result> {
        let url = format!("https://api.example.com/users/{}", user_id);
        let response = self.client.get(&url)?;
        
        // Парсим JSON (упрощённо)
        if response.contains("\"name\":\"") {
            let start = response.find("\"name\":\"").unwrap() + 8;
            let end = response[start..].find('"').unwrap();
            Ok(response[start..start + end].                    

Рекомендаций