Это краткое руководство покажет вам, как выполнять базовое модульное тестирование с помощью Rust.

Репозиторий GitHub

Чтобы проверить окончательный код, загляните в репозиторий GitHub, который я создал для этой статьи.



Модульное тестирование в Rust

По сравнению с другими языками программирования подход модульного тестирования в Rust немного отличается. Таким образом, вместо выделенных тестовых файлов (например, файлов .spec.ts или .test.ts в TypeScript) модульные тесты Rust находятся в том же файле, что и тестируемый код, из-за его модульного подхода.

Несмотря на то, что Rust также поддерживает специальные файлы для тестирования, это предназначено только для интеграционного тестирования библиотек, а не для модульного тестирования. Подробнее об этом в другой статье.

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

Код

В отличие от многих других языков программирования, в Rust все средства тестирования уже встроены, и вам не нужно добавлять какие-либо внешние библиотеки.

Таким образом, вы можете создать новый проект Rust с помощью cargo new my_project и начать писать свои тесты.

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

// src/main.rs
// -------------

fn adder(a: i64, b: i64) -> i64 {
    return a + b;
}

fn my_panic() {
    panic!("oops...");
}

fn substract() -> i64 {
    // TODO: logic not yet implemented
    return 4;
}

Как вы уже видите, у нас есть один метод, в котором срабатывает panic, но, к счастью, в Rust есть встроенный способ проверить, был ли вызван ожидаемый panic.

Также у нас есть один метод ( substract() ), который еще не реализован. Мы хотим временно проигнорировать тест для этого метода, так как мы еще не готовы его протестировать.

Вы можете найти код для тестирования вышеуказанных функций в следующем фрагменте кода в том же файле.

// src/main.rs
// -------------

// ......

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

    #[test]
    fn test_adder() {
        assert_eq!(adder(2, 2), 4);
    }

    #[test]
    #[should_panic(expected = "oops...")]
    fn test_my_panic() {
        my_panic()
    }

    #[test]
    #[ignore]
    fn test_substract() {
        // This test is ignored for now
        assert_eq!(substract(), 6);
    }
}

Как упоминалось ранее, все тесты инкапсулированы в специальный модуль с именем tests, который компилируется только при конфигурации test. Это гарантирует, что тесты не будут скомпилированы, когда вы создаете фактический двоичный файл приложения.

С помощью use super::*; все функции из внешней области видимости импортируются для вызова в тестах. В качестве альтернативы можно было бы быть здесь очень явным и использовать use crate::{adder, my_panic, substract};.

Во-первых, функция adder вызывается в тесте test_adder. Тест украшен атрибутом #[test], чтобы указать исполнителю теста, что эта функция должна выполняться как часть набора тестов. Макрос assert_eq!(a,b) используется для обеспечения того, чтобы одно значение равнялось другому. Если значения не равны, тест не пройден. Чтобы проверить неравенство, вы должны использовать assert_ne!(a,b). Если вы хотите иметь более сложное выражение для проверки, вы можете использовать assert!(expression), что приведет к сбою теста, если выражение оценивается как false.

Для функции test_my_panic добавляется атрибут #[should_panic(expected = "oops...")], чтобы сообщить исполнителю теста, что ожидается panic с сообщением "oops...". Если panic не запускается или сообщение panic отличается от ожидаемого, тест завершится неудачно. Часть expected из should_panic является необязательной, и если ее не добавлять, каждый вызываемый panic будет проходить тест.

Для третьей тестовой функции test_substract тест завершится ошибкой, потому что substract() всегда возвращает 4, поскольку реальная бизнес-логика еще не реализована, а тест ожидает 6. Чтобы предотвратить сбой этого теста, добавляется атрибут #[ignore], чтобы пропустить этот тест в тестовом прогоне.

Запуск тестов

Объяснив, что делают отдельные тесты, и показав различные полезные атрибуты, давайте запустим тесты, чтобы убедиться, что они работают должным образом.

Это можно сделать с помощью cargo test, чтобы запустить все тесты, найденные в проекте.

И, как и ожидалось, два теста прошли, а один был проигнорирован.

Запуск отдельных тестов

Если в данный момент вы работаете только над одним конкретным тестом и для сокращения времени тестирования, вы также можете запустить только один конкретный тест, указав имя теста через cargo test <test_name> , например. cargo test test_adder.

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

Запуск игнорируемых тестов

Если вы хотите запустить все игнорируемые тесты, есть способ сделать это.

По команде cargo test -- --ignored запускаются все игнорируемые тесты, а не игнорируемые - нет.

Флаг --ignored также можно комбинировать с явным определением имени теста.

Следующее также будет работать и будет запускать только игнорируемый тест test_substract:

cargo test test_substract -- --ignored

Хотите связаться?

Если вы хотите связаться со мной, пожалуйста, напишите мне в LinkedIn.

Кроме того, не стесняйтесь проверить мои книжные рекомендации 📚.