동등성 검사와 실패 메시지
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! 매크로를 이용해 테스트하는 예제입니다.
assert_eq! 매크로를 이용한
add_two 함수 테스트
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! 매크로를 이용한 접합’절에서 다루었습니다.)
따라서 {} 자리표시자가 들어있는 포맷 문자열과 자리표시자에 들어갈 값을 전달할 수 있습니다.
커스텀 메시지는 테스트 단언의 의미를 서술하는 데에 유용합니다; 테스트가 실패할 경우 코드의 문제점이 무엇인지 알아내기 더 수월해지죠.
예를 들어 이름을 불러 사람을 환영하는 함수가 있고, 함수에게 전달한 이름이 결과에 나타나는지 확인하는 테스트를 작성한다고 칩시다.
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`
실제 테스트 결괏값을 볼 수 있으니 의도했던 것과 무엇이 다른지 알 수 있어, 디버깅하는 데 도움이 됩니다.