Эта статья является шестнадцатой частью серии из 51 части, поэтому, если вы не читали предыдущие части этой серии, это так.



Вот ссылка на начало серии, если вы хотите начать читать оттуда.



Автоматизированное тестирование

Автоматизированное тестирование является важной областью современной разработки программного обеспечения. Rust явно выбирает собственный набор инструментов для автоматизации тестирования как часть своей приверженности безопасному и надежному коду.

Написание тестов

Cargo автоматически создает новый модуль тестирования при создании нового проекта библиотеки. Начните с создания нового проекта библиотеки под названием «Adder».

cargo new adder --lib

Теперь, открыв файл «lib.rs», давайте посмотрим, из чего состоит тест. Самым основным требованием к тесту является функция, которую вы аннотируете с помощью «#[test]». Примером этой аннотации и базового теста является тест «it_works» в «lib.rs».

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

Любая функция, имеющая тестовую аннотацию, запускается, когда мы вызываем

cargo test

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

running 1 test
test tests::it_works … ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Тестовый вывод состоит из шести разделов. В первой строке указывается количество тестов, которые запускает набор тестов. Далее будет группа строк. Для каждого запущенного теста будет одна строка с указанием имени теста и того, пройден он или нет. Попробуйте изменить это, добавив больше тестов или сделав тест неудачным. Следующая строка показывает сводку результатов теста. После этого повторяются те же три группы, итоговые тесты, строка для каждого теста и сводка тестов, но теперь для doc-тестов, о которых я расскажу позже.

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

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

Распечатка ниже будет показана.

running 3 tests
test tests::it_works … ok
test tests::another2 … FAILED
test tests::another … FAILED
failures:
— — tests::another2 stdout — — 
thread ‘tests::another2’ panicked at ‘assertion failed: `(left == right)`
 left: `4`,
 right: `5`’, src/lib.rs:15:9
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
— — tests::another stdout — — 
thread ‘tests::another’ panicked at ‘assertion failed: `(left == right)`
 left: `4`,
 right: `5`’, src/lib.rs:10:9
failures:
 tests::another
 tests::another2
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass ‘ — lib’

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

Утверждения

Rust использует группу макросов «assert» для проверки тестов. «Assert_eq» принимает два значения и проверяет их на равенство. Если два значения равны, ничего не происходит, и тест проходит успешно. Но если два значения не равны, утверждение вызовет панику, и тест завершится неудачно.

Самый простой из макросов утверждений — это «assert». Этот макрос принимает логическое значение и вызывает панику, если значение ложно. Давайте посмотрим на какой-нибудь знакомый код.

#[derive(Debug)]
pub struct Rectangle {
    length: u32,
    width: u32, 
}
impl Rectangle {
    pub fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

Поскольку метод «can_hold» возвращает логическое значение, мы можем передать это значение прямо в «assert».

#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle { length: 8, width: 7 }; 
        let smaller = Rectangle { length: 5, width: 1 };
         assert!(larger.can_hold(&smaller)); 
    }
}

Если бы мы хотели проверить обратное этой функции, что меньшее не может удерживать большее, мы бы отрицали логическое значение, возвращаемое методом «can_hold».

#[test]
fn smaller_cannot_hold_larger() {
    let larger = Rectangle { length: 8, width: 7 };
    let smaller = Rectangle { length: 5, width: 1 };
    assert!(!smaller.can_hold(&larger));
}

Assert полезен, если у нас есть только одно логическое значение. Но обычно мы хотим убедиться, что два значения равны или не равны. Эти два распространенных варианта использования имеют стандартные функции «assert_eq» и ​​«assert_ne». Посмотрите, как мы используем их со структурой «Прямоугольник» из предыдущего примера.

#[derive(Debug, PartialEq)]
pub struct Rectangle {
    length: u32,
    width: u32, 
}
pub fn add_two_length(rect: &mut Rectangle) -> () { 
    rect.length += 2;
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() {
        let mut rect = Rectangle { length: 8, width: 7 };
        add_two_length(&mut rect);
        assert_eq!(rect, Rectangle { length: 10, width: 7 });
    }
    #[test]
    fn it_works_2() {
        let mut rect = Rectangle { length: 8, width: 7 };
        add_two_length(&mut rect);
       assert_ne!(rect, Rectangle { length: 8, width: 7 });
    } 
}

Обратите внимание, что «Rectangle» теперь должен быть производным от «PartialEq», а также от «Debug». Это сделано для того, чтобы функции утверждения могли сравнивать значения «Прямоугольник» и печатать их, если сравнение не удается.

Паническое тестирование

Тестирование также можно выполнить с помощью аннотации «should_panic». Использование «should_panic» позволяет вам тестировать случаи ошибок, которые должен обрабатывать ваш код.

pub fn rectangle_factory(length: u32, width: u32) -> Rectangle {
    if length == width {
        panic!(“No squares allowed!”);
    }
 
    Rectangle{length, width}
}

Функция «rectangle_factory» создает «Прямоугольники», но сначала проверяет, что длина и ширина не равны. Если два значения совпадают, функция будет паниковать. Мы можем проверить случай сбоя этой функции, используя аннотацию «should_panic».

#[test]
#[should_panic]
fn square() {
    let x = rectangle_factory(5, 5);
}

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

pub fn rectangle_factory(length: u32, width: u32) -> Rectangle {
    if length == width {
        panic!(“No squares allowed!”);
    } else if length == width*2 {
        panic!(“Invalid shape configuration.”)
    }
 
    Rectangle{length, width}
}

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

#[test]
#[should_panic(expected = “No squares allowed!”)]
fn square() {
    let x = rectangle_factory(5, 5);
}
#[test]
#[should_panic(expected = “Invalid shape”)]
fn double_length() {
    let x = rectangle_factory(10, 5);
}

Каждый из этих тестов гарантирует функциональную панику. Тесты также гарантируют, что мы получим правильную панику, передав «ожидаемый» аргумент в «should_panic». Обратите внимание, что вторая функция дает только часть сообщения о панике. «Should_panic» выполняет сопоставление подстроки, что обеспечивает большую гибкость при попытке сопоставить несколько паникеров.

Резюме

Сегодня я показал вам основы написания модульных тестов на Rust и Cargo. Завтра я расскажу более подробно о том, как запускать эти тесты и как вы можете писать другие типы тестов.