동등성 검사와 실패 메시지
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_ne!가 통과한다는 사실만으로 정확한 출력값까지 알 수는 없습니다. 원 add_two가 버그로 3을 더하면 입력 2의 결과는 5입니다. assert_ne!(2, add_two(2))는 ‘입력과 다르다’는 약한 조건을 통과하지만, 요구한 4를 확인하는 assert_eq!(4, add_two(2))는 실패합니다. 결과가 달라져야 한다는 요구와 정확히 2를 더해야 한다는 요구를 구별하세요.
또한 assert_eq!에서 좌우 순서를 바꾸면 비교 결과는 같아도 실패 메시지의 left·right 값이 바뀝니다. assert_eq!(add_two(2), 4)라면 이 버그의 진단은 왼쪽 5·오른쪽 4입니다. 좌우 이름 자체가 예상값·실제값을 뜻하지 않으므로 팀 안에서 읽기 쉬운 순서를 일관되게 쓰면 좋습니다.
독립 연습: 버그가 있는 add_two(10)은 13을 돌려줍니다. assert_eq!(12, add_two(10))와 assert_ne!(10, add_two(10))의 통과 여부를 각각 설명하세요. 이어 원 greeting("Carol")이 "Welcome Carol"로 바뀌었을 때, ‘Carol을 포함한다’와 ‘정확히 Hello Carol!을 반환한다’ 중 어느 계약을 보장하는지 구별하세요. 단언 매크로를 강한 것으로 바꾸는 일이 항상 요구사항을 더 잘 검사하는 일인가요?
검사한 계약만 결론 내리기
12와 13이 달라 첫 단언은 실패하고, 10과 13은 달라 둘째 단언은 통과합니다. 둘째 통과는 2를 더했다는 근거가 아닙니다. "Welcome Carol"에는 이름이 있으므로 원 contains("Carol") 테스트는 통과하지만 정확한 문자열 계약을 만족하지는 않습니다. 요구사항이 이름 포함만 정했다면 문구 전체를 고정하는 테스트가 불필요하게 실패를 만들 수도 있습니다. 실제 요구를 먼저 정하고 그것을 검사할 단언을 골라야 합니다.
커스텀 실패 메시지 추가하기
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`
실제 테스트 결괏값을 볼 수 있으니 의도했던 것과 무엇이 다른지 알 수 있어, 디버깅하는 데 도움이 됩니다.