본문 바로가기

동등성 검사와 실패 메시지

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

테스트 작성 방법에서는 assert!로 조건이 참인지 확인했습니다. 기능을 확인하는 흔한 방법은 테스트할 코드의 결과와 기대하는 값이 같은지 비교하는 것입니다. assert!에 ==를 쓴 식을 넘겨도 되지만, 이 테스트는 아주 흔하므로 표준 라이브러리에는 이를 편하게 해 주는 두 매크로 assert_eq!와 assert_ne!가 있습니다. 이 글에서는 두 매크로와, 실패했을 때 보여 줄 메시지를 직접 정하는 방법을 살펴봅니다.

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

두 매크로는 두 인수가 같은지, 다른지를 비교합니다. 단언이 실패하면 두 값도 함께 출력해 주므로 테스트가 왜 실패했는지 알기 쉽습니다. 반대로 assert!는 == 식이 false가 되었다는 것만 알려 줄 뿐, 그렇게 된 값은 보여 주지 않습니다. 매개변수에 2를 더하는 add_two 함수를 쓰고 assert_eq!로 테스트합니다.

src/lib.rs
pub fn add_two(a: u64) -> u64 {
    a + 2
}

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

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}
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

add_two(2)의 결과를 담은 result를 assert_eq!의 첫 인수로, 4를 둘째 인수로 넘겼으며, 테스트가 통과합니다. 버그를 넣어 assert_eq!가 실패하면 어떻게 보이는지 봅니다. add_two가 3을 더하도록 바꿉니다.

pub fn add_two(a: u64) -> u64 {
    a + 3
}
running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----

thread 'tests::it_adds_two' (6017887) panicked at src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
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

테스트가 버그를 잡았습니다. tests::it_adds_two가 실패했고, 메시지는 left == right라는 단언이 실패했다고 하며 left와 right의 값을 보여 줍니다. left는 add_two(2)의 결과 5이고, right는 기대한 4입니다. 테스트가 많을 때 이렇게 값을 보여 주면 디버깅에 큰 도움이 됩니다.

어떤 언어와 테스트 프레임워크에서는 동등을 확인하는 함수의 매개변수를 expected와 actual로 부르고, 인수를 주는 순서가 중요합니다. 러스트에서는 left와 right라고 부르며, 기대하는 값과 코드가 만든 값의 순서는 상관없습니다. 이 테스트의 단언을 assert_eq!(4, result)로 써도 되며, 그때 실패 메시지는 left에 4, right에 5를 보여 줍니다.

assert_ne!는 두 값이 다르면 통과하고 같으면 실패합니다. 어떤 값이 될지는 확실히 모르지만 무엇이 되면 안 되는지는 분명할 때 쓸모가 있습니다. 예를 들어 입력을 어떤 식으로든 바꾸는 함수를 테스트하는데, 그 결과가 요일에 따라 달라진다면, 함수의 결과가 입력과 같지 않다고 단언하는 것이 가장 알맞을 수 있습니다.

내부적으로 assert_eq!와 assert_ne!는 ==와 != 연산자를 씁니다. 단언이 실패하면 두 매크로는 인수를 디버그 형식으로 출력하므로, 비교하는 값은 PartialEq와 Debug 트레이트를 구현해야 합니다. 기본 타입과 표준 라이브러리의 타입은 대부분 이 두 트레이트를 구현하지만, 직접 정의한 구조체와 열거형은 값이 같은지 확인하려면 PartialEq를, 단언이 실패했을 때 값을 출력하려면 Debug를 구현해야 합니다. 두 트레이트는 모두 파생할 수 있는 트레이트이므로, 대개 정의에 #[derive(PartialEq, Debug)]를 붙이기만 하면 됩니다. 파생할 수 있는 트레이트는 부록 C 파생 가능한 트레이트에 정리되어 있습니다.

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

assert!, assert_eq!, assert_ne!에 실패 메시지를 직접 줄 수도 있습니다. 필수 인수 뒤에 적은 인수는 모두 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이 이름을 빠뜨리도록 바꿉니다.

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

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

이 결과는 단언이 실패했다는 것과 그 줄만 알려 줍니다. greeting이 돌려준 값을 함께 보여 주면 더 쓸모 있을 것입니다. greeting에서 받은 실제 값으로 자리 표시를 채우는 형식 문자열을 직접 정한 실패 메시지로 넘깁니다.

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

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

이제 테스트 결과에서 실제로 받은 값 Hello!를 볼 수 있으므로, 기대한 일 대신 무슨 일이 일어났는지 디버깅하기 쉽습니다. 단언에 메시지를 줄 때는 무엇을 확인하는지와 실패하게 만든 실제 값이 드러나도록 쓰는 것이 좋습니다.

정리

  • assert_eq!(a, b)는 두 값이 같기를, assert_ne!(a, b)는 다르기를 단언하며, 실패하면 left와 right의 값을 보여 줍니다.
  • 러스트에서는 기대하는 값과 실제 값을 어느 순서로 넘겨도 됩니다.
  • 비교하는 값은 PartialEq와 Debug를 구현해야 하며, 직접 만든 타입에는 #[derive(PartialEq, Debug)]를 붙입니다.
  • 단언 매크로의 필수 인수 뒤에 형식 문자열과 값을 주면, 실패했을 때 그 메시지를 출력합니다.

연습 문제

1. 실패 메시지의 값

add_two가 a * 2를 돌려주도록 바뀌었고, 테스트는 let result = add_two(3); assert_eq!(result, 5);입니다. 실패 메시지의 left와 right는 무엇인가요? add_two(2)로 테스트했다면 어떻게 될까요?

해설 보기

add_two(3)은 6이므로 left: 6, right: 5로 실패합니다. add_two(2)로 테스트했다면 2 * 2와 2 + 2가 모두 4여서 테스트가 통과해 버그를 놓칩니다. 입력을 하나만 고르면 우연히 맞는 값에 속을 수 있으므로, 여러 입력으로 확인하는 것이 좋습니다.

2. 직접 만든 타입 비교하기

struct Point { x: i32, y: i32 }의 두 값을 assert_eq!로 비교하려 했더니 컴파일되지 않았습니다. 무엇을 더해야 하나요?

해설 보기

assert_eq!는 ==로 비교하고 실패하면 값을 디버그 형식으로 출력하므로, Point가 PartialEq와 Debug를 구현해야 합니다. 정의 앞에 #[derive(PartialEq, Debug)]를 붙이면 필드끼리 비교하고 출력하는 구현이 만들어집니다.

다음 글 패닉과 Result 테스트에서는 코드가 패닉을 내야 하는 상황을 확인하는 should_panic과, Result를 돌려주는 테스트를 살펴봅니다.