본문으로 건너뛰기

안동민 개발노트

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

패닉과 Result 테스트

should_panic과 Result 반환 테스트로 실패 조건과 오류 메시지를 검증합니다.

should_panic 매크로로 패닉 발생 검사하기

코드의 반환 값을 검사하는 것에 더하여, 예상한대로 에러 조건을 잘 처리하는지 검사하는 것도 중요합니다.

예를 들어 8장의 예제 8-13에서 만들었던 Guess 타입을 생각해보세요.

Guess 타입을 사용하는 다른 코드는 Guess 인스턴스가 1에서 100 사잇값임을 보장하는 기능에 의존합니다.

이런 경우, 범위를 벗어난 값으로 Guess 인스턴스를 만들면 패닉이 발생하는지 검사하는 테스트를 작성하면 이를 확실하게 보장할 수 있습니다.

패닉 검사 테스트 함수에는 should_panic 속성을 추가합니다.

이 테스트는 내부에서 패닉이 발생해야 통과되고, 패닉이 발생하지 않으면 실패합니다.

예제 10-8은 Guess::new의 에러 조건이 의도대로 작동하는지 검사하는 테스트를 보여줍니다.

예제 10-8: panic! 발생 테스트
src/lib.rs
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] 속성과 적용할 함수 사이에 위치시켰습니다.

테스트 성공 시 결과를 살펴봅시다.

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

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

   Doc-tests guessing_game

running 0 tests

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

괜찮아 보이네요!

이제 new 함수의 패닉 발생 조건 중 100보다 큰 값일 때의 조건을 지워서 버그를 만들어 보죠.

// --생략--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

예제 10-8 테스트를 실행하면 다음과 같이 실패합니다.

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures.

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures.
    tests::greater_than_100

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`

에러 메시지는 그다지 유용하지 않지만, 테스트 함수를 살펴보면 #[should_panic]으로 어노테이션된 함수라는 걸 알 수 있습니다.

즉, 테스트 함수에서 패닉이 발생하지 않아서 실패했다는 뜻이죠.

should_panic을 사용하는 테스트는 정확하지 않을 수 있습니다.

의도한 것과는 다른 이유로 패닉이 발생하더라도 should_panic 테스트는 통과할 것입니다.

should_panic 속성에 expected 매개변수를 추가해, 포함되어야 하는 실패 메시지를 지정하면 더 꼼꼼한 should_panic 테스트를 작성할 수 있습니다.

예제 10-9는 new 함수에서 값이 너무 작은 경우와 큰 경우에 서로 다른 메시지로 panic!을 발생시키도록 수정한 Guess 코드입니다.

예제 10-9: 특정한 부분 문자열을 포함하는 패닉 메시지를 사용한 panic!에 대한 테스트
src/lib.rs
// --생략--

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.이 되겠죠.

expected 매개변수에 명시할 내용은 패닉 메시지가 얼마나 고유한지 혹은 동적인지, 그리고 테스트에 요구되는 정확성에 따라 달라집니다.

이번 경우에는, 패닉 메시지 문자열 일부만으로도 실행된 함수 코드가 else if value > 100 상황에 해당함을 확신할 수 있으니 충분합니다.

expected 메시지를 지정한 should_panic 테스트가 실패하면 어떻게 되는지 알아보죠.

if value < 1 코드 단락과 else if value > 100 코드 단락을 서로 바꾸어 버그를 만들어 보았습니다.

        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
            );
        }

이번에는 should_panic 테스트가 실패합니다.

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures.

---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
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"`

failures.
    tests::greater_than_100

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`

테스트에서 패닉이 발생하긴 했지만, 지정한 "less than or equal to 100" 문자열이 패닉 메시지에 포함되어 있지 않다는 것을 알려줍니다.

실제로 발생한 패닉 메시지는 Guess value must be greater than or equal to 1, got 200.입니다.

이제 이 메시지를 단서로 버그를 찾아낼 수 있습니다!


Result<T, E>를 이용한 테스트

지금까지는 실패 시 패닉을 발생시키는 테스트만 작성했습니다.

테스트는 Result<T, E>를 사용해 작성할 수도 있습니다.

다음은 예제 10-1 테스트를 Result<T, E>를 사용하도록 수정한 예시입니다.

패닉을 발생시키는 대신 Err을 반환합니다.

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 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를 반환할 경우 실패해야 하는 테스트를 작성하기 편리합니다.

Result<T, E> 테스트에서는 #[should_panic] 어노테이션을 사용할 수 없습니다.

연산이 Err 배리언트를 반환하는 것을 단언하기 위해서는 Result<T, E> 값에 물음표 연산자를 사용하지 마세요.

대신 assert!(value.is_err())를 사용하세요.

여러 테스트 작성 방법을 배웠으니, 테스트를 실행할 때 어떤 일들이 일어나는지 알아보고 cargo test 명령어 옵션을 살펴봅시다.


테스트 작성 방법 학습 판단 기준표

테스트 작성 방법를 구현할 때 컴파일러가 막는 지점과 테스트로 확인할 지점을 분리해 봅니다.