패닉과 Result 테스트
should_panic과 Result 반환 테스트로 실패 조건과 오류 메시지를 검증합니다.
코드가 올바른 값을 돌려주는지 확인하는 것과 함께, 오류 상황을 기대한 대로 다루는지 확인하는 것도 중요합니다. 이 글에서는 코드가 패닉을 내야 하는 상황을 확인하는 should_panic 속성과, 실패했을 때 패닉 대신 Err를 돌려주는 Result 테스트를 살펴봅니다.
should_panic 속성으로 패닉 발생 검사하기
panic! 사용 기준에서 본 패닉하는 Guess::new를 생각해 봅니다. Guess를 쓰는 다른 코드는 Guess 인스턴스가 언제나 1부터 100 사이의 값만 담는다는 데 기댑니다. 그러므로 범위 밖의 값으로 Guess를 만들면 패닉이 나는지 확인하는 테스트를 쓸 수 있습니다.
이를 위해 테스트 함수에 should_panic 속성을 붙입니다. 이 테스트는 함수 안의 코드가 패닉을 내면 통과하고, 패닉을 내지 않으면 실패합니다.
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}#[should_panic]은 #[test] 뒤, 테스트 함수 앞에 둡니다. 이 테스트를 실행하면 통과합니다.
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s이제 값이 100보다 클 때 패닉을 내는 조건을 지워 코드에 버그를 넣습니다. 아래 new에는 value < 1을 검사하는 조건만 남았으므로, 테스트가 넘기는 100보다 큰 값은 패닉 없이 그대로 Guess가 됩니다.
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s메시지 끝의 src/lib.rs:21:8은 패닉을 기대한 테스트 함수 greater_than_100이 정의된 곳입니다. 메시지 자체는 별로 쓸모 있지 않지만, 그 테스트 함수를 보면 #[should_panic]이 붙어 있습니다. 실패했다는 것은 테스트 함수 안의 코드가 패닉을 내지 않았다는 뜻입니다.
should_panic 테스트는 정확하지 않을 수 있습니다. 기대한 까닭이 아닌 다른 까닭으로 패닉이 나도 통과하기 때문입니다. 더 정확하게 하려면 should_panic에 선택 매개변수 expected를 붙입니다. 그러면 테스트 실행기가 실패 메시지에 지정한 문자열이 들어 있는지 확인합니다. 다음 Guess::new는 값이 너무 작을 때와 너무 클 때 서로 다른 메시지로 패닉을 냅니다.
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}should_panic의 expected에 적은 값이 Guess::new가 내는 패닉 메시지의 일부이므로 이 테스트는 통과합니다. 패닉 메시지 전체인 Guess value must be less than or equal to 100, got 200.을 적어도 되지만, 어디까지 적을지는 패닉 메시지 가운데 얼마나 고유하거나 바뀌는 부분인지, 그리고 테스트를 얼마나 정확하게 하고 싶은지에 따라 정합니다. 여기서는 패닉 메시지의 일부만으로도 함수가 else if value > 100 쪽을 실행했다는 것을 확인하기에 충분합니다.
expected가 있는 should_panic 테스트가 실패하면 어떻게 되는지 보기 위해, if value < 1과 else if value > 100 블록의 본문을 서로 바꿔 버그를 넣습니다.
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
Guess { value }
}
}running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' (6017887) panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "Guess value must be greater than or equal to 1, got 200."
expected substring: "less than or equal to 100"실패 메시지는 테스트가 기대한 대로 패닉을 내기는 했지만, 패닉 메시지에 기대한 문자열 less than or equal to 100이 들어 있지 않았다고 알려 줍니다. 실제로 받은 패닉 메시지는 Guess value must be greater than or equal to 1, got 200.이므로, 이를 보고 버그가 어디 있는지 찾을 수 있습니다.
Result<(), E>를 이용한 테스트
지금까지의 테스트는 실패하면 모두 패닉을 냈습니다. 테스트에서 Result<T, E>를 쓸 수도 있습니다. 다음은 앞의 add 테스트를 패닉 대신 Result<T, E>로 Err를 돌려주도록 고친 것입니다.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}it_works의 반환 타입이 Result<(), String>이 되었습니다. 본문에서 assert_eq!를 부르는 대신, 테스트가 통과하면 Ok(())를, 실패하면 안에 String을 담은 Err를 돌려줍니다. 테스트가 Result<T, E>를 돌려주게 하면 본문에서 물음표 연산자를 쓸 수 있으므로, 안의 연산이 Err를 돌려주면 테스트가 실패하도록 편하게 쓸 수 있습니다.
#[cfg(test)]
mod tests {
#[test]
fn parses_port() -> Result<(), std::num::ParseIntError> {
let port: u16 = "8080".parse()?;
assert_eq!(port, 8080);
Ok(())
}
}"8080".parse()가 실패하면 ?가 그 Err를 돌려주어 테스트가 실패하고, 성공하면 꺼낸 값으로 단언을 이어 갑니다. Result<T, E>를 쓰는 테스트에는 #[should_panic]을 붙일 수 없습니다. 어떤 연산이 Err를 돌려주는지 확인하고 싶다면, Result<T, E> 값에 ?를 쓰지 말고 assert!(value.is_err())처럼 단언합니다. 아래 표는 확인하려는 것에 따라 이 글과 앞 글의 도구 가운데 무엇을 고를지 정리한 것입니다.
Rust · test contract · semantic table
실패 모양이 아니라 검증할 계약에 맞춰 도구를 고른다
예상 밖 오류와 예상한 오류를 먼저 구분합니다. 성공해야 하는 준비 작업의 Err는 ?로 실패시키지만, 오류 자체가 기대 결과라면 ?를 쓰지 않고 종류와 내용을 직접 단언합니다.
| 검증할 계약 | 도구와 적합한 질문 | 통과·실패 근거와 경계 |
|---|---|---|
bool |
assert!(condition) · 조건 하나가 참이어야 하는가? | true면 계속하고 false면 패닉으로 실패합니다. 필요하면 커스텀 메시지를 붙입니다.조건은 truthy 값이 아니라 실제 |
| 값 비교 | assert_eq! · assert_ne! · 두 값을 함께 봐야 하는가? | 비교 계약이 깨지면 패닉으로 실패하고 좌우 값을 Debug 표현으로 보여줍니다.비교에는 |
| 패닉 | #[should_panic(expected = "...")] · 특정 패닉 계약인가? | 함수가 패닉하고 지정 문자열이 패닉 메시지의 부분 문자열이면 통과합니다. 반환 타입은 |
| 준비 오류 | fn test() -> Result<(), E>와 ? · 파일 읽기와 파싱이 성공해야 하는가? | Ok(())는 성공, 전파된 Err(E)는 Termination을 통해 실패로 보고됩니다.
|
| 기대 오류 | assert!(matches!(...)) 또는 오류 값을 꺼낸 뒤 구체적으로 단언 · 어느 오류인가? | ?를 쓰지 않고 오류 variant, 필드, 메시지 등 계약한 내용을 비교해 통과 여부를 정합니다.
|
boolean contract
assert!
실제 bool 조건 하나가 참이어야 할 때 씁니다. true면 계속하고 false면 패닉으로 실패하며 커스텀 메시지를 붙일 수 있습니다.
equality contract
assert_eq! · assert_ne!
두 값의 동등 또는 부등이 계약이고 실패 시 좌우 값을 함께 봐야 할 때 씁니다. 비교에는 PartialEq, 진단 출력에는 Debug가 필요합니다.
panic contract
#[should_panic] + expected
테스트가 패닉하고 expected = "..."의 문자열이 패닉 메시지의 부분 문자열이면 통과합니다. 반환 타입은 ()여야 하며, 다른 패닉이 같은 문자열을 포함할 가능성은 남습니다.
fallible setup
Result<(), E> + ?
성공해야 하는 읽기와 파싱의 Err(E)를 곧바로 테스트 실패로 전파합니다. Ok(())는 성공이며 E: Debug가 필요하고 #[should_panic]과 결합하지 않습니다.
expected error
기대한 Err에는 ?를 쓰지 않기
assert!(matches!(...))로 오류 형태를 단언하거나 오류 값을 꺼내 종류, 필드, 메시지를 구체적으로 단언합니다. is_err()만으로는 variant 여부만 확인합니다.
#[test] 함수의 반환 타입은 Termination을 구현해야 합니다. 일반적인 Result<(), E> 테스트에서 Ok(())는 성공으로, Err(E)는 실패로 보고되며 E는 Debug를 구현해야 합니다.
조건이 참인지는 assert!로, 두 값이 같거나 다른지는 assert_eq!와 assert_ne!로, 특정한 패닉이 나는지는 expected를 붙인 should_panic으로 확인합니다. 성공해야 하는 준비 작업은 Result를 돌려주는 테스트에서 ?로 쓰고, 오류가 나는 것 자체를 확인하려면 is_err()나 오류의 종류를 직접 단언합니다. 테스트를 쓰는 여러 방법을 알았으니, 다음 글에서는 테스트를 실행할 때 무슨 일이 일어나는지와 cargo test의 여러 옵션을 봅니다.
정리
#[should_panic]을 붙인 테스트는 함수 안에서 패닉이 나면 통과하고, 나지 않으면test did not panic as expected로 실패합니다.should_panic(expected = "...")는 패닉 메시지에 그 문자열이 들어 있는지까지 확인하므로, 엉뚱한 까닭의 패닉을 걸러 냅니다.- 테스트가
Result<(), E>를 돌려주면Ok(())로 통과,Err로 실패하며, 본문에서?를 쓸 수 있습니다. Result를 돌려주는 테스트에는should_panic을 쓸 수 없고,Err를 확인하려면assert!(value.is_err())처럼 단언합니다.
연습 문제
1. 엉뚱한 패닉
#[should_panic]만 붙인 테스트가 Guess::new(200) 대신 실수로 Guess::new(0)을 부르고 있었습니다. 이 테스트는 통과하나요? expected = "less than or equal to 100"을 붙였다면 어떻게 되나요?
해설 보기
#[should_panic]만 붙였다면 0도 범위 밖이라 패닉이 나므로 통과하며, 100보다 큰 값을 확인하려던 의도와 다른데도 실수를 알아채지 못합니다. expected를 붙였다면 0은 greater than or equal to 1 메시지로 패닉을 내므로 기대한 문자열이 들어 있지 않아 실패하고, 실수를 드러냅니다.
2. 어떤 테스트로 쓸까
(가) 설정 파일 문자열을 파싱한 뒤 그 값을 확인하는 테스트에서, 파싱이 실패하면 테스트도 실패해야 한다. (나) 빈 문자열을 넘기면 파싱 함수가 Err를 돌려주는지 확인하고 싶다. 각각 어떻게 쓰나요?
해설 보기
(가)는 테스트가 Result를 돌려주게 하고 파싱에 ?를 붙인 뒤 꺼낸 값을 assert_eq!로 확인합니다. 파싱이 실패하면 ?가 Err를 돌려주어 테스트가 실패합니다. (나)는 Err가 나오는 것 자체가 기대한 결과이므로 ?를 쓰지 않고, assert!(parse("").is_err())처럼 결과를 직접 단언합니다.
다음 글 테스트 실행 제어에서는 테스트를 병렬이나 순서대로 실행하는 방법, 함수의 출력을 보는 방법, 일부 테스트만 고르거나 무시하는 방법을 살펴봅니다.