테스트 실행 방식 제어하기

cargo run이 코드를 컴파일한 후 결과 바이너리를 실행하는 것처럼, cargo test는 코드를 테스트 모드로 컴파일한 후 결과 테스트 바이너리를 실행한다. cargo test로 생성된 바이너리의 기본 동작은 모든 테스트를 병렬로 실행하고, 테스트 실행 중 생성된 출력을 캡처하여 화면에 표시하지 않는다. 이렇게 하면 테스트 결과와 관련된 출력을 더 쉽게 읽을 수 있다. 하지만 커맨드라인 옵션을 지정해 이 기본 동작을 변경할 수 있다.

일부 커맨드라인 옵션은 cargo test에 전달되고, 다른 옵션은 결과 테스트 바이너리에 전달된다. 이 두 가지 유형의 인자를 구분하려면, cargo test에 전달할 인자를 먼저 나열한 후 구분자 --를 넣고, 그 다음에 테스트 바이너리에 전달할 인자를 나열한다. cargo test --help를 실행하면 cargo test와 함께 사용할 수 있는 옵션을 확인할 수 있고, cargo test -- --help를 실행하면 구분자 뒤에 사용할 수 있는 옵션을 확인할 수 있다. 이 옵션들은 rustc 책“Tests” 섹션에 문서화되어 있다.

테스트를 병렬 또는 순차적으로 실행하기

여러 테스트를 실행할 때 기본적으로 스레드를 사용해 병렬로 실행한다. 이렇게 하면 테스트가 더 빨리 완료되고 결과를 빠르게 확인할 수 있다. 하지만 테스트가 동시에 실행되기 때문에, 각 테스트가 서로 의존하거나 공유 상태(예: 현재 작업 디렉토리나 환경 변수와 같은 공유 환경)에 의존하지 않도록 주의해야 한다.

예를 들어, 각 테스트가 디스크에 _test-output.txt_라는 파일을 생성하고 데이터를 쓰는 코드를 실행한다고 가정하자. 그런 다음 각 테스트는 해당 파일의 데이터를 읽고 파일에 특정 값이 포함되어 있는지 확인한다. 이때 각 테스트는 서로 다른 값을 검사한다. 테스트가 동시에 실행되면 한 테스트가 파일을 쓰는 동안 다른 테스트가 파일을 덮어쓸 수 있다. 이 경우 두 번째 테스트는 코드가 잘못된 것이 아니라 병렬 실행 중에 테스트가 서로 간섭했기 때문에 실패할 수 있다. 이를 해결하는 한 가지 방법은 각 테스트가 서로 다른 파일에 쓰도록 하는 것이다. 또 다른 방법은 테스트를 하나씩 순차적으로 실행하는 것이다.

테스트를 병렬로 실행하고 싶지 않거나 사용할 스레드 수를 더 세밀하게 제어하고 싶다면 --test-threads 플래그와 사용할 스레드 수를 테스트 바이너리에 전달할 수 있다. 다음 예제를 살펴보자:

$ cargo test -- --test-threads=1

테스트 스레드 수를 1로 설정해 프로그램이 병렬 처리를 사용하지 않도록 한다. 하나의 스레드로 테스트를 실행하면 병렬로 실행할 때보다 시간이 더 오래 걸리지만, 테스트가 상태를 공유하더라도 서로 간섭하지 않는다.

함수 출력 보여주기

기본적으로 테스트가 성공하면, Rust의 테스트 라이브러리는 표준 출력에 출력된 모든 내용을 캡처한다. 예를 들어, 테스트에서 println!을 호출하고 테스트가 성공하면, 터미널에서 println!의 출력을 볼 수 없다. 대신 테스트가 성공했다는 메시지만 보게 된다. 테스트가 실패하면, 실패 메시지와 함께 표준 출력에 출력된 내용을 확인할 수 있다.

예를 들어, 리스트 11-10에는 매개변수의 값을 출력하고 10을 반환하는 단순한 함수가 있다. 이 함수에 대한 테스트 중 하나는 성공하고, 다른 하나는 실패한다.

Filename: src/lib.rs
fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {a}");
    10
}

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

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(value, 10);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(value, 5);
    }
}
Listing 11-10: println!을 호출하는 함수에 대한 테스트

cargo test로 이 테스트를 실행하면 다음과 같은 출력을 볼 수 있다:

$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

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`

출력 결과에서 I got the value 4라는 메시지를 찾을 수 없다. 이 메시지는 테스트가 성공할 때 출력되지만, 캡처되었다. 반면, 실패한 테스트의 출력인 I got the value 8은 테스트 요약 출력 부분에 나타나며, 테스트 실패의 원인도 함께 표시된다.

성공한 테스트의 출력값도 확인하고 싶다면, --show-output 플래그를 사용해 Rust에게 성공한 테스트의 출력도 보여주도록 지시할 수 있다:

$ cargo test -- --show-output

--show-output 플래그를 사용해 리스트 11-10의 테스트를 다시 실행하면, 다음과 같은 출력을 볼 수 있다:

$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
I got the value 4


successes:
    tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

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`

이름으로 테스트의 일부만 실행하기

전체 테스트 스위트를 실행하면 시간이 오래 걸릴 수 있다. 특정 코드 영역을 작업 중이라면 해당 코드와 관련된 테스트만 실행하고 싶을 수 있다. cargo test에 실행하려는 테스트의 이름을 인자로 전달하면 원하는 테스트만 선택적으로 실행할 수 있다.

테스트의 일부만 실행하는 방법을 보여주기 위해, 먼저 add_two 함수에 대한 세 가지 테스트를 작성한다. Listing 11-11에서 이를 확인할 수 있으며, 어떤 테스트를 실행할지 선택할 수 있다.

Filename: src/lib.rs
pub fn add_two(a: usize) -> usize {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }

    #[test]
    fn add_three_and_two() {
        let result = add_two(3);
        assert_eq!(result, 5);
    }

    #[test]
    fn one_hundred() {
        let result = add_two(100);
        assert_eq!(result, 102);
    }
}
Listing 11-11: 서로 다른 이름을 가진 세 가지 테스트

앞서 보았듯이, 아무런 인자 없이 테스트를 실행하면 모든 테스트가 병렬로 실행된다:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 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

단일 테스트 실행하기

cargo test 명령어에 특정 테스트 함수의 이름을 전달하면 해당 테스트만 실행할 수 있다:

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

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

one_hundred라는 이름의 테스트만 실행되었고, 나머지 두 테스트는 이름이 일치하지 않아 실행되지 않았다. 테스트 출력 결과 끝부분에 2 filtered out이라는 메시지가 표시되어 실행되지 않은 테스트가 두 개 있음을 알려준다.

이 방식으로는 여러 테스트의 이름을 동시에 지정할 수 없다. cargo test에 전달된 첫 번째 값만 사용되기 때문이다. 하지만 여러 테스트를 실행할 수 있는 방법이 있다.

여러 테스트를 실행하기 위한 필터링

테스트 이름의 일부를 지정하면 해당 값과 일치하는 이름을 가진 모든 테스트가 실행된다. 예를 들어, 두 테스트의 이름에 add가 포함되어 있으므로 cargo test add를 실행하면 해당 두 테스트만 실행할 수 있다:

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

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

이 커맨드는 이름에 add가 포함된 모든 테스트를 실행하고 one_hundred라는 이름의 테스트는 제외했다. 또한 테스트가 속한 모듈 이름도 테스트 이름의 일부가 되므로, 모듈 이름으로 필터링하면 해당 모듈의 모든 테스트를 실행할 수 있다.

특별히 요청하지 않는 한 일부 테스트 무시하기

때로는 특정 테스트를 실행하는 데 시간이 오래 걸릴 수 있다. 이 경우 cargo test를 실행할 때 대부분의 경우 해당 테스트를 제외하고 싶을 수 있다. 실행하고 싶은 모든 테스트를 인수로 나열하는 대신, 시간이 오래 걸리는 테스트에 ignore 속성을 추가해 제외할 수 있다.

파일명: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
}

#[test] 뒤에 제외하고 싶은 테스트에 #[ignore] 줄을 추가한다. 이제 테스트를 실행하면 it_works는 실행되지만 expensive_test는 실행되지 않는다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 1 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

expensive_test 함수는 ignored로 표시된다. 만약 무시된 테스트만 실행하고 싶다면 cargo test -- --ignored를 사용할 수 있다.

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 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

어떤 테스트를 실행할지 제어함으로써 cargo test 결과를 빠르게 확인할 수 있다. ignored 테스트의 결과를 확인할 필요가 있고, 결과를 기다릴 시간이 있다면 cargo test -- --ignored를 실행하면 된다. 무시된 테스트를 포함해 모든 테스트를 실행하고 싶다면 cargo test -- --include-ignored를 사용할 수 있다.