테스트 작성 방법

Beginner

This tutorial is from open-source community. Access the source code

소개

테스트 작성 방법에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 속성 (attribute), 매크로 (macro), 그리고 어서션 (assertion) 을 사용하여 Rust 에서 테스트를 작성하는 방법에 대해 배우겠습니다.

테스트 작성 방법

테스트는 비 (非) 테스트 코드가 예상대로 작동하는지 확인하는 Rust 함수입니다. 테스트 함수의 본문은 일반적으로 다음 세 가지 작업을 수행합니다.

  • 필요한 데이터 또는 상태를 설정합니다.
  • 테스트하려는 코드를 실행합니다.
  • 결과가 예상과 일치하는지 어서션 (assertion) 합니다.

이러한 작업을 수행하는 테스트를 작성하기 위해 Rust 가 제공하는 기능들을 살펴보겠습니다. 여기에는 test 속성 (attribute), 몇 가지 매크로 (macro), 그리고 should_panic 속성이 포함됩니다.

테스트 함수의 구조

가장 간단하게, Rust 에서 테스트는 test 속성 (attribute) 으로 주석 처리된 함수입니다. 속성은 Rust 코드 조각에 대한 메타데이터입니다. 한 가지 예는 5 장에서 구조체 (struct) 와 함께 사용했던 derive 속성입니다. 함수를 테스트 함수로 변경하려면 fn 앞 줄에 #[test]를 추가합니다. cargo test 명령으로 테스트를 실행하면 Rust 는 주석 처리된 함수를 실행하고 각 테스트 함수가 통과했는지 실패했는지 보고하는 테스트 러너 (test runner) 바이너리를 빌드합니다.

Cargo 로 새로운 라이브러리 프로젝트를 만들 때마다 테스트 함수가 있는 테스트 모듈이 자동으로 생성됩니다. 이 모듈은 테스트를 작성하기 위한 템플릿을 제공하므로 새로운 프로젝트를 시작할 때마다 정확한 구조와 구문을 찾아볼 필요가 없습니다. 원하는 만큼 많은 추가 테스트 함수와 테스트 모듈을 추가할 수 있습니다!

실제로 코드를 테스트하기 전에 템플릿 테스트를 실험하여 테스트가 작동하는 방식의 몇 가지 측면을 살펴보겠습니다. 그런 다음 작성한 코드를 호출하고 해당 동작이 올바른지 어서션 (assertion) 하는 실제 테스트를 작성합니다.

두 숫자를 더하는 adder라는 새로운 라이브러리 프로젝트를 만들어 보겠습니다.

$ cargo new adder --lib
Created library $(adder) project
$ cd adder

adder 라이브러리의 src/lib.rs 파일의 내용은 Listing 11-1 과 같아야 합니다.

파일 이름: src/lib.rs

#[cfg(test)]
mod tests {
  1 #[test]
    fn it_works() {
        let result = 2 + 2;
      2 assert_eq!(result, 4);
    }
}

Listing 11-1: cargo new에 의해 자동으로 생성된 테스트 모듈 및 함수

지금은 처음 두 줄을 무시하고 함수에 집중해 보겠습니다. #[test] 주석 [1]에 주목하십시오. 이 속성은 이것이 테스트 함수임을 나타내므로 테스트 러너는 이 함수를 테스트로 처리해야 함을 알고 있습니다. 또한 일반적인 시나리오를 설정하거나 일반적인 작업을 수행하는 데 도움이 되는 비 (非) 테스트 함수를 tests 모듈에 가질 수 있으므로 어떤 함수가 테스트인지 항상 표시해야 합니다.

예제 함수 본문은 assert_eq! 매크로 [2]를 사용하여 2 와 2 를 더한 결과가 포함된 result가 4 와 같은지 어서션 (assertion) 합니다. 이 어서션은 일반적인 테스트 형식의 예로 사용됩니다. 이 테스트가 통과하는지 확인하기 위해 실행해 보겠습니다.

cargo test 명령은 Listing 11-2 와 같이 프로젝트의 모든 테스트를 실행합니다.

[object Object]

Listing 11-2: 자동으로 생성된 테스트를 실행한 결과

Cargo 는 테스트를 컴파일하고 실행했습니다. running 1 test [1] 줄을 볼 수 있습니다. 다음 줄에는 생성된 테스트 함수의 이름인 it_works와 해당 테스트 실행 결과가 ok [2]임을 보여줍니다. 전체 요약 test result: ok. [3]는 모든 테스트가 통과했음을 의미하며, 1 passed; 0 failed 부분은 통과하거나 실패한 테스트의 총 개수를 나타냅니다.

특정 인스턴스에서 실행되지 않도록 테스트를 무시하도록 표시할 수 있습니다. "특정 요청이 없는 한 일부 테스트 무시"에서 다룰 것입니다. 여기서는 그렇게 하지 않았으므로 요약에 0 ignored가 표시됩니다. 또한 cargo test 명령에 인수를 전달하여 이름이 문자열과 일치하는 테스트만 실행할 수 있습니다. 이를 필터링(filtering) 이라고 하며 "이름으로 테스트의 하위 집합 실행"에서 다룰 것입니다. 여기서는 실행되는 테스트를 필터링하지 않았으므로 요약의 끝에 0 filtered out이 표시됩니다.

0 measured 통계는 성능을 측정하는 벤치마크 테스트에 대한 것입니다. 이 글을 쓰는 시점에서 벤치마크 테스트는 nightly Rust 에서만 사용할 수 있습니다. 자세한 내용은 https://doc.rust-lang.org/unstable-book/library-features/test.html에서 벤치마크 테스트에 대한 문서를 참조하십시오.

Doc-tests adder [4]로 시작하는 테스트 출력의 다음 부분은 모든 문서 테스트의 결과입니다. 아직 문서 테스트가 없지만 Rust 는 API 문서에 나타나는 모든 코드 예제를 컴파일할 수 있습니다. 이 기능은 문서와 코드를 동기화하는 데 도움이 됩니다! "테스트로서의 문서 주석"에서 문서 테스트를 작성하는 방법에 대해 논의할 것입니다. 지금은 Doc-tests 출력을 무시하겠습니다.

테스트를 사용자 정의 요구 사항에 맞게 시작해 보겠습니다. 먼저 it_works 함수의 이름을 exploration과 같이 다른 이름으로 변경합니다.

파일 이름: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

그런 다음 cargo test를 다시 실행합니다. 이제 출력에 it_works 대신 exploration이 표시됩니다.

running 1 test
test tests::exploration ... ok

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

이제 다른 테스트를 추가하겠지만 이번에는 실패하는 테스트를 만들겠습니다! 테스트 함수의 무언가가 패닉 (panic) 하면 테스트가 실패합니다. 각 테스트는 새로운 스레드에서 실행되며, 메인 스레드가 테스트 스레드가 종료된 것을 감지하면 테스트는 실패로 표시됩니다. 9 장에서 panic! 매크로를 호출하는 것이 패닉하는 가장 간단한 방법이라고 이야기했습니다. another라는 함수로 새 테스트를 입력하면 src/lib.rs 파일이 Listing 11-3 과 같습니다.

파일 이름: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

Listing 11-3: panic! 매크로를 호출하므로 실패할 두 번째 테스트 추가

cargo test를 사용하여 테스트를 다시 실행합니다. 출력은 Listing 11-4 와 같아야 하며, exploration 테스트가 통과하고 another가 실패했음을 보여줍니다.

running 2 tests
test tests::exploration ... ok
1 test tests::another ... FAILED

2 failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

3 failures:
    tests::another

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

error: test failed, to rerun pass '--lib'

Listing 11-4: 하나의 테스트가 통과하고 하나의 테스트가 실패했을 때의 테스트 결과

ok 대신 test tests::another 줄에 FAILED [1]가 표시됩니다. 개별 결과와 요약 사이에 두 개의 새로운 섹션이 나타납니다. 첫 번째 [2]는 각 테스트 실패에 대한 자세한 이유를 표시합니다. 이 경우 anothersrc/lib.rs 파일의 10 번째 줄에서 panic at 'Make this test fail'로 인해 실패했다는 세부 정보를 얻습니다. 다음 섹션 [3]은 실패한 모든 테스트의 이름만 나열하며, 테스트가 많고 자세한 실패 테스트 출력이 많은 경우 유용합니다. 실패한 테스트의 이름을 사용하여 해당 테스트만 실행하여 더 쉽게 디버깅할 수 있습니다. "테스트 실행 방법 제어"에서 테스트를 실행하는 방법에 대해 자세히 설명합니다.

요약 줄이 마지막에 표시됩니다 [4]: 전반적으로 테스트 결과는 FAILED입니다. 하나의 테스트가 통과하고 하나의 테스트가 실패했습니다.

이제 다양한 시나리오에서 테스트 결과가 어떻게 보이는지 확인했으므로 panic! 외에 테스트에 유용한 다른 매크로를 살펴보겠습니다.

assert! 매크로로 결과 확인하기

표준 라이브러리에서 제공하는 assert! 매크로는 테스트에서 어떤 조건이 true로 평가되는지 확인하려는 경우에 유용합니다. assert! 매크로에 부울 (Boolean) 로 평가되는 인수를 제공합니다. 값이 true이면 아무 일도 일어나지 않고 테스트가 통과합니다. 값이 false이면 assert! 매크로는 panic!을 호출하여 테스트가 실패하도록 합니다. assert! 매크로를 사용하면 코드가 의도한 방식으로 작동하는지 확인할 수 있습니다.

Listing 5-15 에서 Rectangle 구조체와 can_hold 메서드를 사용했는데, 이는 Listing 11-5 에 다시 반복됩니다. 이 코드를 src/lib.rs 파일에 넣은 다음 assert! 매크로를 사용하여 몇 가지 테스트를 작성해 보겠습니다.

파일 이름: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Listing 11-5: 5 장에서 Rectangle 구조체와 해당 can_hold 메서드 사용

can_hold 메서드는 부울을 반환하므로 assert! 매크로에 완벽한 사용 사례입니다. Listing 11-6 에서 너비가 8 이고 높이가 7 인 Rectangle 인스턴스를 생성하고 너비가 5 이고 높이가 1 인 다른 Rectangle 인스턴스를 포함할 수 있는지 어서션 (assertion) 하여 can_hold 메서드를 실행하는 테스트를 작성합니다.

파일 이름: src/lib.rs

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

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

Listing 11-6: 더 큰 사각형이 실제로 더 작은 사각형을 포함할 수 있는지 확인하는 can_hold에 대한 테스트

tests 모듈 내에 새로운 줄 use super::*; [1]을 추가했음에 유의하십시오. tests 모듈은 "모듈 트리에서 항목을 참조하기 위한 경로"에서 다룬 일반적인 가시성 규칙을 따르는 일반 모듈입니다. tests 모듈은 내부 모듈이므로 외부 모듈에서 테스트 중인 코드를 내부 모듈의 범위로 가져와야 합니다. 여기서는 glob 을 사용하므로 외부 모듈에서 정의한 모든 항목을 이 tests 모듈에서 사용할 수 있습니다.

테스트 이름을 larger_can_hold_smaller [2]로 지정하고 필요한 두 개의 Rectangle 인스턴스를 생성했습니다 [3]. 그런 다음 assert! 매크로를 호출하고 larger.can_hold(&smaller) [4]를 호출한 결과를 전달했습니다. 이 표현식은 true를 반환해야 하므로 테스트가 통과해야 합니다. 확인해 봅시다!

running 1 test
test tests::larger_can_hold_smaller ... ok

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

통과했습니다! 이번에는 더 작은 사각형이 더 큰 사각형을 포함할 수 없음을 어서션 (assertion) 하는 다른 테스트를 추가해 보겠습니다.

파일 이름: src/lib.rs

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

    #[test]
    fn larger_can_hold_smaller() {
        --snip--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

이 경우 can_hold 함수의 올바른 결과는 false이므로 assert! 매크로에 전달하기 전에 해당 결과를 부정해야 합니다. 결과적으로 can_holdfalse를 반환하면 테스트가 통과합니다.

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

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

두 개의 테스트가 통과했습니다! 이제 코드에 버그를 도입했을 때 테스트 결과가 어떻게 되는지 살펴보겠습니다. 너비 비교 시 부등호를 <로 바꿔서 can_hold 메서드의 구현을 변경합니다.

--snip--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

이제 테스트를 실행하면 다음과 같은 결과가 생성됩니다.

running 2 tests
test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread 'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::larger_can_hold_smaller

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

테스트가 버그를 잡았습니다! larger.width8이고 smaller.width5이므로 can_hold에서 너비 비교는 이제 false를 반환합니다. 8 은 5 보다 작지 않기 때문입니다.

assert_eq!assert_ne! 매크로를 사용한 동등성 테스트

기능을 확인하는 일반적인 방법은 테스트 중인 코드의 결과와 코드가 반환할 것으로 예상하는 값 간의 동등성을 테스트하는 것입니다. assert! 매크로를 사용하고 == 연산자를 사용하는 표현식을 전달하여 이 작업을 수행할 수 있습니다. 그러나 이것은 매우 일반적인 테스트이므로 표준 라이브러리는 이 테스트를 보다 편리하게 수행하기 위해 assert_eq!assert_ne!라는 한 쌍의 매크로를 제공합니다. 이러한 매크로는 각각 두 인수를 동등성 또는 비동등성에 대해 비교합니다. 또한 어서션 (assertion) 이 실패하면 두 값을 인쇄하여 테스트가 실패했는지 더 쉽게 확인할 수 있습니다. 반대로, assert! 매크로는 == 표현식에 대해 false 값을 얻었다는 것만 나타내며, false 값을 초래한 값을 인쇄하지 않습니다.

Listing 11-7 에서 매개변수에 2를 더하는 add_two라는 함수를 작성한 다음 assert_eq! 매크로를 사용하여 이 함수를 테스트합니다.

파일 이름: src/lib.rs

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

Listing 11-7: assert_eq! 매크로를 사용하여 add_two 함수 테스트

통과하는지 확인해 봅시다!

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

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
}

테스트를 다시 실행합니다.

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----
1 thread 'main' panicked at 'assertion failed: `(left == right)`
2   left: `4`,
3  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

테스트가 버그를 잡았습니다! it_adds_two 테스트가 실패했고, 메시지는 실패한 어서션이 assertion failed:(left == right)`[1]이고left[2] 및right[3] 값이 무엇인지 알려줍니다. 이 메시지는 디버깅을 시작하는 데 도움이 됩니다. left인수는4였지만 add_two(2)가 있는 right인수는5`였습니다. 테스트가 많이 진행될 때 특히 도움이 될 것이라고 상상할 수 있습니다.

일부 언어 및 테스트 프레임워크에서 동등성 어서션 (assertion) 함수의 매개변수를 expectedactual이라고 부르며, 인수를 지정하는 순서가 중요합니다. 그러나 Rust 에서는 leftright라고 부르며, 예상하는 값과 코드가 생성하는 값을 지정하는 순서는 중요하지 않습니다. 이 테스트에서 어서션을 assert_eq!(add_two(2), 4)로 작성할 수 있으며, 이는 assertion failed:(left == right)``을 표시하는 동일한 실패 메시지를 생성합니다.

assert_ne! 매크로는 제공된 두 값이 같지 않으면 통과하고 같으면 실패합니다. 이 매크로는 값이 무엇인지 확실하지 않지만 값이 확실히 아니어야 하는 경우에 가장 유용합니다. 예를 들어, 입력을 어떤 방식으로 변경하는 것이 보장되지만 입력이 변경되는 방식이 테스트를 실행하는 요일에 따라 달라지는 함수를 테스트하는 경우, 어서션하는 가장 좋은 방법은 함수의 출력이 입력과 같지 않다는 것입니다.

내부적으로 assert_eq!assert_ne! 매크로는 각각 ==!= 연산자를 사용합니다. 어서션이 실패하면 이러한 매크로는 디버그 형식 지정을 사용하여 인수를 인쇄합니다. 즉, 비교되는 값은 PartialEqDebug 트레이트를 구현해야 합니다. 모든 기본 유형과 대부분의 표준 라이브러리 유형은 이러한 트레이트를 구현합니다. 직접 정의하는 구조체 (struct) 및 열거형 (enum) 의 경우 해당 유형의 동등성을 어서션 (assertion) 하려면 PartialEq를 구현해야 합니다. 또한 어서션이 실패할 때 값을 인쇄하려면 Debug를 구현해야 합니다. 두 트레이트 모두 Listing 5-12 에서 언급했듯이 파생 가능한 트레이트이므로 일반적으로 #[derive(PartialEq, Debug)] 주석을 구조체 또는 열거형 정의에 추가하는 것만큼 간단합니다. 이러한 파생 가능한 트레이트에 대한 자세한 내용은 부록 C 를 참조하십시오.

사용자 지정 실패 메시지 추가

assert!, assert_eq!, 및 assert_ne! 매크로에 선택적 인수로 실패 메시지와 함께 인쇄할 사용자 지정 메시지를 추가할 수도 있습니다. 필수 인수 뒤에 지정된 모든 인수는 "더하기 연산자 또는 format! 매크로를 사용한 연결"에서 논의된 format! 매크로로 전달되므로, {} 자리 표시자와 해당 자리 표시자에 들어갈 값을 포함하는 형식 문자열을 전달할 수 있습니다. 사용자 지정 메시지는 어서션 (assertion) 의 의미를 문서화하는 데 유용합니다. 테스트가 실패하면 코드에 어떤 문제가 있는지 더 잘 알 수 있습니다.

예를 들어, 사람들에게 이름을 사용하여 인사를 하는 함수가 있고 함수에 전달하는 이름이 출력에 나타나는지 테스트하려는 경우를 가정해 보겠습니다.

파일 이름: 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 함수에서 반환된 값과 정확한 동등성을 확인하는 대신 출력에 입력 매개변수의 텍스트가 포함되어 있는지 어서션 (assertion) 합니다.

이제 greeting을 변경하여 name을 제외하여 이 코드에 버그를 도입하여 기본 테스트 실패가 어떻게 보이는지 살펴보겠습니다.

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

이 테스트를 실행하면 다음이 생성됩니다.

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' 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

이 결과는 어서션이 실패했고 어서션이 있는 줄을 나타냅니다. 더 유용한 실패 메시지는 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 'main' 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

테스트 출력에서 실제로 얻은 값을 볼 수 있으며, 이는 예상되는 것이 아니라 발생한 문제를 디버깅하는 데 도움이 됩니다.

should_panic을 사용한 패닉 확인

반환 값을 확인하는 것 외에도 코드가 예상대로 오류 조건을 처리하는지 확인하는 것이 중요합니다. 예를 들어, Listing 9-13 에서 생성한 Guess 유형을 생각해 보십시오. Guess를 사용하는 다른 코드는 Guess 인스턴스가 1 에서 100 사이의 값만 포함한다는 보장에 의존합니다. 해당 범위를 벗어난 값으로 Guess 인스턴스를 생성하려고 하면 패닉이 발생하는지 확인하는 테스트를 작성할 수 있습니다.

이 작업은 테스트 함수에 should_panic 속성을 추가하여 수행합니다. 함수 내부의 코드가 패닉하면 테스트가 통과하고, 함수 내부의 코드가 패닉하지 않으면 테스트가 실패합니다.

Listing 11-8 은 Guess::new의 오류 조건이 예상대로 발생하는지 확인하는 테스트를 보여줍니다.

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

Listing 11-8: 조건이 패닉을 유발하는지 테스트!

#[test] 속성 뒤와 적용되는 테스트 함수 앞에 #[should_panic] 속성을 배치합니다. 이 테스트가 통과할 때의 결과를 살펴보겠습니다.

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

좋아 보입니다! 이제 new 함수가 값이 100 보다 큰 경우 패닉하는 조건을 제거하여 코드에 버그를 도입해 보겠습니다.

// src/lib.rs
--snip--

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

        Guess { value }
    }
}

Listing 11-8 에서 테스트를 실행하면 실패합니다.

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

이 경우 매우 유용한 메시지를 얻지 못하지만, 테스트 함수를 살펴보면 #[should_panic]으로 주석 처리되어 있음을 알 수 있습니다. 우리가 얻은 실패는 테스트 함수의 코드가 패닉을 유발하지 않았음을 의미합니다.

should_panic을 사용하는 테스트는 부정확할 수 있습니다. should_panic 테스트는 예상했던 것과 다른 이유로 테스트가 패닉하는 경우에도 통과합니다. should_panic 테스트를 더 정확하게 만들려면 should_panic 속성에 선택적 expected 매개변수를 추가할 수 있습니다. 테스트 하네스는 실패 메시지에 제공된 텍스트가 포함되어 있는지 확인합니다. 예를 들어, Listing 11-9 에서 new 함수가 값이 너무 작거나 너무 큰지에 따라 다른 메시지로 패닉하는 Guess에 대한 수정된 코드를 고려하십시오.

// src/lib.rs
--snip--

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

Listing 11-9: 지정된 부분 문자열을 포함하는 패닉 메시지로 panic! 테스트

이 테스트는 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 < 1else if value > 100 블록의 본문을 바꿔서 코드에 다시 버그를 도입해 보겠습니다.

// src/lib.rs
--snip--
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
    );
}
--snip--

이번에는 should_panic 테스트를 실행하면 실패합니다.

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

failures:

---- tests::greater_than_100 stdout ----
thread 'main' 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

실패 메시지는 이 테스트가 실제로 예상대로 패닉했지만 패닉 메시지에 예상 문자열 'Guess value must be less than or equal to 100'이 포함되지 않았음을 나타냅니다. 이 경우 실제로 얻은 패닉 메시지는 Guess value must be greater than or equal to 1, got 200이었습니다. 이제 버그가 있는 위치를 파악하기 시작할 수 있습니다!

테스트에서 Result<T, E> 사용하기

지금까지의 모든 테스트는 실패할 때 패닉합니다. Result<T, E>를 사용하는 테스트도 작성할 수 있습니다! 다음은 Listing 11-1 의 테스트를 Result<T, E>를 사용하고 패닉하는 대신 Err를 반환하도록 다시 작성한 것입니다.

파일 이름: src/lib.rs

#[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 변형을 반환하는지 어서션 (assertion) 하려면 Result<T, E> 값에 물음표 연산자를 사용하지 마십시오. 대신 assert!(value.is_err())를 사용하십시오.

이제 테스트를 작성하는 여러 가지 방법을 알았으므로, 테스트를 실행할 때 어떤 일이 발생하는지 살펴보고 cargo test와 함께 사용할 수 있는 다양한 옵션을 살펴보겠습니다.

요약

축하합니다! 테스트 작성 방법 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.