Как стать автором
Обновить
1099.38
OTUS
Цифровые навыки от ведущих экспертов

Кратко про юнит-тесты в Rust

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров3.2K

Привет!

Юнит-тесты позволяют предотвратить ошибки и значительно упростить процессы рефакторинга и поддержки кода. Их реализация существует во всех языках программирования и Rust - не исключение.

Юнит-тесты в Rust обычно располагаются в том же файле, что и тестируемый код, в специальном модуле с именем tests, аннотированном #[cfg(test)]. Внутри этого модуля размещаются функции тестирования, каждая из которых также аннотируется как #[test].

Пример простого юнит-теста в Rust:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

tests содержит функцию it_works, которая проверяет, что операция сложения выполняется корректно. Если условие assert_eq!(2 + 2, 4) не выполняется, тест считается не пройденным.

Или пример немного посложней - функция для деления двух чисел, которая должна возвращать ошибку при попытке деления на ноль:

fn safe_divide(dividend: i32, divisor: i32) -> Result<i32, String> {
    if divisor == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(dividend / divisor)
    }
}

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

    #[test]
    fn test_divide_by_zero() {
        let result = safe_divide(10, 0);
        assert_eq!(result, Err("Division by zero".to_string()));
    }

    #[test]
    fn test_normal_division() {
        let result = safe_divide(10, 2);
        assert_eq!(result, Ok(5));
    }
}

В Rust нет ограничений на тестирование приватных функций, что позволяет выбирать, тестировать ли их напрямую или нет. Пример теста приватной функции:

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(internal_adder(2, 2), 4);
    }
}

В этом случае тестируется приватная функция internal_adder, что возможно с use super::*, позволяющему тестам видеть содержимое родительского модуля.

Ассерты

Макрос assert! используется для проверки, что булево выражение истинно. Если выражение оценивается как false, происходит паника, и тест считается неудачным. Этот макрос может также принимать пользовательское сообщение для вывода в случае ошибки:

assert!(1 + 1 == 2);
assert!(some_boolean_function(), "Expected true but got false");

Макрос assert_eq! проверяет равенство двух выражений, используя трейт PartialEq. Если значения не равны, тест паникует и выводит значения обеих выражений, что помогает быстро понять причину несоответствия. Можно добавить собственное сообщение для вывода дополнительных данных:

let expected = 2;
let result = 1 + 1;
assert_eq!(result, expected, "Testing addition: {} + 1 should be {}", 1, expected);

Макрос assert_ne! используется для проверки, что два выражения не равны. Как и assert_eq!, при несоответствии значения выводятся для упрощения отладки, и возможно добавление пользовательского сообщения:

let a = 3;
let b = 4;
assert_ne!(a, b, "Values should not be equal: {} and {}", a, b);

Mock объекты и зависимости

С помощью атрибута #[automock], mockall может автоматически создать мок для любого трейта. Это упрощает тестирование, т.к не требуется ручное определение мок-структур:

#[automock]
trait MyTrait {
    fn foo(&self, x: u32) -> u32;
}

В тестах можно настроить поведение моков, используя методы expect_ для определения ожидаемых вызовов и возвращаемых значений:

#[test]
fn mytest() {
    let mut mock = MockMyTrait::new();
    mock.expect_foo()
        .with(eq(4))
        .times(1)
        .returning(|x| x + 1);
    assert_eq!(5, mock.foo(4));
}

Если автоматическое создание моков не подходит (например, при необходимости более сложной конфигурации), можно использовать макрос mock!:

mock! {
    pub MyStruct<T: Clone + 'static> {
        fn bar(&self) -> u8;
    }
    impl<T: Clone + 'static> MyTrait for MyStruct<T> {
        fn foo(&self, x: u32);
    }
}

После создания моков можно настроить их поведение в тестах, задавая ожидаемые вызовы, аргументы и возвращаемые значения, так можно тестировать разные сценарии:

let mut mock = MockMyTrait::new();
mock.expect_foo()
    .return_const(44u32);
mock.expect_bar()
    .with(predicate::ge(1))
    .returning(|x| x + 1);

Можно рассмотреть такой пример:

use mockall::{automock, predicate::*};

struct Database {
              ....
}

#[automock]
impl Database {
    fn get_user(&self, user_id: i32) -> Option<String> {
        // определенные операции
        Some("User".to_string())
    }
}

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

    #[test]
    fn test_get_user() {
        let mut mock_db = MockDatabase::new();
        mock_db.expect_get_user()
               .with(eq(42))
               .times(1)
               .returning(|_| Some("User".to_string()));

        let result = mock_db.get_user(42);
        assert_eq!(result, Some("User".to_string()));
    }

Подробнее про mockall можно прочитать по ссылке.

Snapshot тестирование

Когда впервые запускаются тесты с использованием insta, тесты скорее всего упадут, поскольку отсутствуют базовые снимки для сравнения. В этом случае insta создаёт новые снимки с расширением .snap.new. Можно просмотреть эти снимки и, если они соответствуют ожидаемым результатам, подтвердить их как основные снимки для будущих тестов.

При помощи команды cargo insta review можно интерактивно просматривать изменения между старыми и новыми снимками и выбирать, принимать ли новые результаты или отклонять их.

insta автоматически обнаруживает, когда тесты запущены в CI, и изменяет поведение обновления снимков, чтобы предотвратить случайное изменение снимков без явного подтверждения.

Макросы для создания снимков:

assert_snapshot! для базовых строковых снимков.

assert_debug_snapshot! для снимков, которые используют формат вывода Debug для объектов.

assert_yaml_snapshot!, assert_json_snapshot! и другие, поддерживающие сериализацию данных в различные форматы (требуется включение соответствующих функций).

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

Пример:

#[test]
fn test_vector() {
    let data = vec![1, 2, 3];
    insta::assert_debug_snapshot!(data);
}

После первого запуска теста insta создаст файл снимка, который можно просмотреть и подтвердить, если вывод соответствует ожиданиям. При последующих изменениях функции можно наблюдать за тем, какие изменения произошли, и либо принять их, либо исправить.

Параметризованные тесты

Для реализации параметризованных тестов часто используется крейт rstest. Этот крейт предоставляет макросы для определения тестов с несколькими наборами параметров.

Допустим, есть функция add, которая просто возвращает сумму двух чисел. Напишем для неё параметризованный тест, который проверит функцию на разных наборах входных данных:

use rstest::rstest;

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[rstest]
#[case(1, 2, 3)]
#[case(5, -2, 3)]
#[case(0, 0, 0)]
fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

Макрос #[rstest] используется для создания параметризованного теста. Аннотация #[case(...)] определяет отдельные наборы параметров, каждый из которых приведет к генерации отдельного теста.

Теперь создадим параметризованный тест для функции, которая выполняет деление с проверкой на деление на ноль:

use rstest::rstest;
use std::num::NonZeroU32;

fn safe_divide(numerator: u32, denominator: NonZeroU32) -> u32 {
    numerator / denominator.get()
}

#[rstest]
#[case(10, NonZeroU32::new(2).unwrap(), 5)]
#[case(20, NonZeroU32::new(5).unwrap(), 4)]
#[case(12, NonZeroU32::new(3).unwrap(), 4)]
fn test_safe_divide(#[case] numerator: u32, #[case] denominator: NonZeroU32, #[case] expected: u32) {
    assert_eq!(safe_divide(numerator, denominator), expected);
}

Используется тип NonZeroU32, который гарантирует, что делитель не может быть нулём, тем самым предотвращая возможность ошибки деления на ноль во время выполнения. Каждый #[case] определяет различные входные данные, на которых должна быть проверена функция.


В завершение хочу порекомендовать вам бесплатный вебинар про Borrow checker - это одна из главных фич языка. О ней преподаватели расскажут на вебинаре и полайвкодят вместе с вами.

Теги:
Хабы:
Всего голосов 13: ↑10 и ↓3+11
Комментарии9

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS