본문으로 건너뛰기

안동민 개발노트

본문 시작
10장 : 자동화 테스트 작성하기테스트 작성 방법

동등성 검사와 실패 메시지

assert_eq!·assert_ne!로 동등성을 검사하고 진단 가능한 커스텀 실패 메시지를 작성합니다.

assert_eq!, assert_ne! 매크로를 이용한 동등 테스트

기능성 검증의 일반적인 방법은 테스트 코드의 결괏값이 예상한 값과 같은지 확인하는 것입니다.

이는 assert! 매크로에 == 연산자를 사용한 표현식을 전달하는 식으로도 가능하지만, 러스트는 이런 테스트에 더 알맞은 매크로를 따로 제공합니다.

assert_eq!, assert_ne! 매크로는 각각 두 인수를 비교하고 동등한지(equality) 그렇지 않은지(inequality) 판단합니다.

단언 코드가 실패하면 두 값을 출력하여 테스트의 실패 사유를 더 알기 쉽게 보여줍니다.

assert! 매크로는 == 표현식이 false 값임을 알려줄 뿐, 어떤 값으로 인해 false 값이 나왔는지 출력하지는 않습니다.

예제 10-7은 매개변수에 2를 더하는 add_two 함수를 작성한 다음, assert_eq! 매크로를 이용해 테스트하는 예제입니다.

예제 10-7: assert_eq! 매크로를 이용한 add_two 함수 테스트
src/lib.rs
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}

테스트를 통과하는지 확인해봅시다!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

assert_eq!4를 인수로 넘겼는데, 이는 add_two(2) 호출 결과와 같습니다.

출력 중 테스트에 해당하는 줄은 test tests::it_adds_two ... ok이고, ok는 테스트가 통과했다는 뜻이죠!

코드에 버그를 집어넣어서 assert_eq!가 실패했을 때는 어떤 식으로 보이는지 확인해봅시다.

add_two 함수가 3을 더하도록 구현을 변경해봅시다.

pub fn add_two(a: i32) -> i32 {
    a + 3
}

테스트를 다시 실행해 보죠.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures.

---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)`
  left: `4`,
 right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures.
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

테스트가 버그를 찾아냈습니다!

it_adds_two 테스트가 실패하고, 메시지는 assertion failed: `(left == right)` 메시지와 left, right가 각각 4, 5 였다는 것을 알려줍니다.

이 메시지로 assert_eq!left 인수는 4였는데 right 인수(add_two(2))는 5였다는 내용을 알 수 있기 때문에, 디버깅을 시작하는 데 도움이 됩니다.

수많은 테스트가 있을 때라면 특히 유용할 것임을 짐작할 수 있습니다.

몇몇 프로그래밍 언어, 프레임워크에서는 동등 단언 함수의 매개변수를 expected, actual라고 지칭하며, 코드를 작성할 때 인수의 순서를 지켜야 합니다.

하지만 러스트에서는 left, right라고 지칭할 뿐, 예상값과 테스트 코드로 만들어진 값의 순서는 상관없습니다.

테스트 코드를 assert_eq!(add_two(2), 4) 로 작성할 수도 있습니다.

이 경우에도 실패 메시지는 똑같이 assertion failed: `(left == right)`라고 나타납니다.

assert_ne! 매크로는 전달한 두 값이 서로 같지 않으면 통과하고, 동등하면 실패합니다.

어떤 값이 될지는 확신할 수 없지만, 적어도 이 값은 되지 않아야 함을 알고 있는 경우에 유용합니다.

예를 들어, 테스트할 함수가 입력값을 어떤 방식으로든 변경한다는 것은 확실하지만, 테스트를 실행하는 요일에 따라 함수의 입력값이 달라진다면, 입력값과 함수 출력이 동일하면 안 된다고 테스트를 작성하는 게 가장 좋을 겁니다.

내부적으로 assert_eq!, assert_ne! 매크로는 각각 ==, != 연산자를 사용합니다.

단언에 실패할 경우, 매크로는 인수를 디버그 형식으로 출력하는데, 즉 assert_eq!, assert_ne! 매크로로 비교할 값은 PartialEq, Debug 트레이트를 구현해야 합니다.

모든 기본 타입 및 대부분의 표준 라이브러리 타입은 이 두 트레이트를 구현합니다.

직접 정의한 구조체나 열거형의 경우에는 PartialEq 트레이트를 구현하여 해당 타입의 값이 같음을 단언할 수 있도록 할 필요가 있습니다.

또한 단언 실패 시 값이 출력될 수 있도록 Debug 트레이트도 구현해야 합니다.

4장 예제 4-12에서 설명했듯 두 트레이트 모두 파생 가능한 트레이트이기 때문에, 구조체, 열거형 정의에 #[derive(PartialEq, Debug)]를 어노테이션하는 것이 일반적입니다.

이에 대한 추가 내용 및 파생 가능한 나머지 트레이트는 부록 C ‘파생 가능한 트레이트’를 참고해 주세요.


커스텀 실패 메시지 추가하기

assert!, assert_eq!, assert_ne! 매크로에 추가 인수로 실패 메시지에 출력될 내용을 추가할 수 있습니다.

필수적인 인수들 이후의 인수는 format! 매크로로 전달됩니다. (format! 매크로는 7장의 + 연산자나 format! 매크로를 이용한 접합’절에서 다루었습니다.)

따라서 {} 자리표시자가 들어있는 포맷 문자열과 자리표시자에 들어갈 값을 전달할 수 있습니다.

커스텀 메시지는 테스트 단언의 의미를 서술하는 데에 유용합니다; 테스트가 실패할 경우 코드의 문제점이 무엇인지 알아내기 더 수월해지죠.

예를 들어 이름을 불러 사람을 환영하는 함수가 있고, 함수에게 전달한 이름이 결과에 나타나는지 확인하는 테스트를 작성한다고 칩시다.

src/lib.rs
pub fn greeting(name: &str) -> String {
    format!("Hello {}!", name)
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

아직 프로그램의 요구 사항이 정해지지 않아서, 분명히 Hello 텍스트 부분이 나중에 변경될 거라고 치죠.

프로그램 요구 사항이 바뀔 때 테스트 코드도 고치고 싶지는 않으니 greeting 함수의 정확한 반환 값을 검사하는 대신, 출력 값에 입력 매개변수로 전달한 텍스트가 포함되어 있는지만 확인하려고 합니다.

이제 기본 테스트 실패 시 출력을 살펴보기 위해, greeting 함수 결괏값에서 name이 빠지도록 변경하여 버그를 만들어 보았습니다.

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

테스트 결과는 다음과 같습니다.

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures.

---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures.
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

출력 결과는 단언이 실패했다는 것과 몇 번째 줄에서 실패했는지만 표시합니다.

실패 메시지에서 greeting 함수의 반환 값을 출력해 주면 더 유용하겠죠.

테스트 함수에 커스텀 실패 메시지를 추가해봅시다.

greeting 함수가 반환하는 실제 값으로 채워지게 될 자리표시자가 들어있는 포맷 문자열을 작성해 보죠.

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{}`",
            result
        );
    }

이제 에러 메시지를 보고 더 많은 정보를 얻을 수 있습니다.

테스트를 다시 실행해 보죠.

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures.

---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures.
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

실제 테스트 결괏값을 볼 수 있으니 의도했던 것과 무엇이 다른지 알 수 있어, 디버깅하는 데 도움이 됩니다.