Rust #11: 11장 자동화된 테스트 작성하기

Sep 9, 2021·

16 min read

개요

테스트는 작성된 코드가 의도한 대로 동작하는지 확인하는 작업입니다. Rust는 주석 및 매크로, 테스트 시행을 위해 제공되는 기본 동작 및 옵션, 단위 테스트 및 통합 테스트로 구성하는 방법을 제공합니다.

테스트 작성 방법

테스트는 코드가 예상대로 동작 하는지 확인하는 Rust 함수라고 할 수 있습니다. 테스트 함수 내용은 일반적으로 다음 세 가지 작업을 수행합니다.

  1. 필요한 데이터 또는 상태를 설정
  2. 테스트할 코드를 실행
  3. 결과가 기대한 것인지 주장(ASSERT)

테스트 함수의 해부

Rust에서 테스트 단위는 test 속성으로 주석이 달린 함수라고 할 수 있습니다. cargo test를 통해 테스트 되는 단위이며, 테스트 용 실행 바이너리를 빌드하고 각 테스트 함수의 통과 또는 실패 여부를 보고합니다.

Cargo를 이용해 새 라이브러리 프로젝트를 생성하면 기본 테스트 기능이 포함된 테스트 모듈이 자동 생성됩니다.

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

파일명: src/lib.rs

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

it_works()'함수는#[test]에 의해 테스트 함수가 됩니다.assert_eq!`매크로는 두 값이 같은지 주장(확인)합니다. 같으면 테스트 통과이고 같지 않으면 실패가 됩니다.

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

running 1 test
test tests::it_works ... 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

it_worksexploration으로 변경하면 출력 결과 달라집니다.

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

테스트 함수를 추가해 여려 개의 테스트를 수행할 수 있습니다.

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

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

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

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


failures:
    tests::another

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'

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

assert! 매크로는 true일 때 테스트 성공, false일 때 테스트 실패이며 panic!을 호출합니다.

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

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

        assert!(larger.can_hold(&smaller));
    }
}
$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

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

   Doc-tests rectangle

running 0 tests

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

다음은 더 작은 사각형이 더 큰 사각형을 담을 수 없다고 단언하는 또 다른 테스트를 추가 해보겠습니다.

#[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));
    }
}
$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

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

   Doc-tests rectangle

running 0 tests

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

다음에는 일부러 코드에 버그를 넣어서 이 테스트 결과가 어떻게 달라지는지 봅시다.

// --snip--
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}
$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

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

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

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

버그가 생기면서 정상적으로 동작했던 테스트 함수들의 결과를 통해 버그를 감지할 수 있게 되었습니다!

assert_eq! 및 assert_ne! 매크로를 사용하여 같음 테스트

assert! 매크로 뿐만 아니라 assert_eq!assert_ne! 매크로를 통해 테스트를 진행 할 수 있습니다.

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

4add_two(2)의 결과값(4) 가 같은지 주장합니다. 같으면 테스트 성공, 같지 않으면 테스트 실패가 됩니다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (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

add_two()함수를 다음과 같이 수정해 버그를 만들어 봅시다.

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 (target/debug/deps/adder-92948b65e88960b4)

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

failures:

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

기대했던 결과가 아니므로 테스트는 실패하며 버그를 감지할 수 있습니다!

assert_ne!매크로는 두 값이 동일하지 않을 경우 테스트 성공, 같을 경우 테스트 실패가 됩니다.

같거나 같지않음은 모든 기본 유형과 대부분의 표준 라이브러리 유형에서 구현되어 있지만 사용자 유형의 경우 PartialEq를 구현해야 합니다. 또 실패할 경우 출력하는 기능인 Debug를 구현해야 합니다. 이는 파생 가능한 특성이기 때문에 #[derive(PartialEq, Debug)] 주석을 추가하는 것으로 가능합니다.

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

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

이 코드의 테스트 결과는 성공합니다. 실패하도록 다음처럼 수정해봅시다.

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 (target/debug/deps/greeter-170b942eb5bf5e3a)

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

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'

Rust는 매크로의 기능으로 똑똑하게 result.contains("Carol")이라는 메시지를 출력합니다. 하지만 사용자 메시지로 표시하고자 할 경우, 다음의 코드처럼 할 수 있습니다.

    #[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 (target/debug/deps/greeter-170b942eb5bf5e3a)

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

failures:

---- 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


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'
`

패닉 확인 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);
    }
}
$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (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

should_panic 속성으로 해당 함수가 panic!되었을 때 테스트 성공으로 처리합니다! 이제 버그를 만들어서 이를 감지 하는지 확인해봅시다.

// --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 }
    }
}
$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests (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'

멋지네요. 그런데 panic!이 테스트하고자 하는 원인으로 발생하지 않을 수 있습니다. 이런 경우 테스트 실패가 되어야 하는데 다음 코드처럼 expectedpanic! 문자열을 주어 이것을 구분할 수 있습니다.

// --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 = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

이제 버그를 만들어 봅시다.

        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
            );
        }
$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished test [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

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: `"Guess value must be 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'

테스트 실패로 버그를 감지할 수 있습니다.

Result 테스트에서 사용

다음의 코드를 봅시다.

#[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"))
        }
    }
}

결과가 Ok이면 테스트 성공, Err이면 테스트 실패이며, 메시지가 출력 됩니다. 또한 panic!이 호출되었을 때 테스트가 실패하므로 물음표 연산자를 통해 위의 코드를 좀 더 단순하게 만들 수도 있습니다. Result<T, E>를 반환하는 테스트 함수에서는 #[should_panic]을 사용할 수 없습니다.

테스트 실행 방법 제어

cargo run이 코드를 컴파일 한 후 바이너리를 실행하는 것 처럼 cargo test는 테스트 모드에서 코드를 컴파일하고 테스트 바이너리를 실행합니다. 명령 옵션을 통해 기본 동작을 바꿀 수 있습니다.

병렬 또는 연속으로 테스트 실행

테스트를 더 빨리 끝내기 위해 테스트 스레드를 지정해서 병렬로 테스트를 진행할 수 있습니다.

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

테스트 환경이 병렬로 진행할 수 있는지 확인한 후 테스트 스레드 개수를 늘려 테스트 시간을 단축할 수 있습니다.

함수 출력 표시

특별한 인자를 주지 않을 경우 테스트 중에는 테스트가 성공했을 때는 표준 출력을 캡쳐 하지 않습니다.

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!(10, value);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(5, value);
    }
}
$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests (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 'main' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
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 -- --show-output
$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests (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 'main' panicked at 'assertion failed: `(left == right)`
  left: `5`,
 right: `10`', src/lib.rs:19:9
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'

이름으로 테스트 하위 집합 실행

다음의 테스트 코드가 있을 때,

pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

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

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

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}

테스트를 진행하면 모든 테스트가 수행됩니다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests (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 one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests (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

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

특정 이름으로 두 개 이상의 테스트 함수를 실행할 수 있습니다.

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests (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

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

ignore 속성이 부여되었을 경우 해당 테스트 함수는 전체 테스트에서 제외됩니다.

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

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

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

   Doc-tests adder

running 0 tests

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

하지만 ignore 옵션을 주었을 경우 해당 테스트 함수가 실행되게 됩니다.

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests (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

테스트 구조

테스트는 복잡한 분야이며 사람들마다 다른 용어와 구조를 사용합니다. Rust에서는 단위 테스트와 통합 테스트라는 두 가지 주요 범주에서 테스트를 바라봅니다.

두 종류의 테스트를 작성하는 것은 라이브러리가 자체 또는 이용하는 측면에서 기대하는 대로 동작 하는지 확인하는데 중요합니다.

단위 테스트

단위 테스트의 목적은 자체적으로 각 기능이 정상적으로 동작하는지 확인하는데 목적이 있습니다. 라이브러리 내부의 기능이 잘 동작 하는 것을 확인합니다.

테스트 모듈 및 #[cfg(test)]

#[cfg(test)]cargo build에는 포함되지 않고 cargo test로 테스트 중에만 해당 모듈이 포함되도록 Rust에 지시 합니다. 그러나 통합 테스트는 다른 디렉토리로 이동하기 때문에 #[cfg(test)] 주석이 필요하지 않습니다. 그러나 단위 테스트는 일반 코드와 동일한 코드에 있기 때문에 #[cfg(test)] 주석을 주어 빌드 시 바이너리에 포함되지 않도록 합니다.

앞 전에 addr 라이브러리 프로젝트를 생성했을 떄 다음과 같이 단위 테스트 코드가 생성된 것을 확인할 수 있었습니다.

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

비공개 기능 테스트

비공개 기능을 직접 테스트해야 하는지에 대한 논쟁이 있지만 Rust는 이를 허용합니다.

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

통합 테스트

통합 테스트는 라이브러리가 정상적으로 동작하는지 대상인 라이브러리의 외부에서 테스트를 진행합니다. 마치 라이브러리를 이용하는 것처럼 동일한 방식으로 공개된 API를 호출합니다. 그러므로 단위 테스트와는 다르게 비공개 기능에는 접근할 수 없습니다. 실제로 사용하는 관점에서 테스트를 수행하므로 통합 테스트 역시 단위 테스트 만큼 중요합니다.

tests 디렉토리

Rust는 tests 디렉토리가 통합 테스트를 위한 디렉토리라는 것을 알고 있습니다.

파일 이름: tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

라이브러리와는 별도의 테스트이므로 #[cfg(test)] 주석이 빠진다는 것을 알 수 있습니다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test 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

파일 단위의 테스트를 수행하기 위해 다음과 같이 인자를 주어 테스트를 진행할 수 있습니다.

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

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

통합 테스트의 하위 모듈

통합 테스트에도 일반 코드 모듈처럼 하위 모듈로 구성해서 테스트를 진행할 수 있습니다. 다음은 테스트 함수에서 초기화 등을 이유로 공통으로 호출해야 하는 함수가 있을 경우 common.rs등으로 만들고 호출해서 사용할 수 있습니다.

파일 이름: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

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

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test 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

그런데 테스트 대상에 해당 함수가 포함되었습니다. 우리가 원하는 결과는 아닙니다. 이것을 해결하려면 tests/common.rs 대신 tests/common/mod.rs의 위치에 만듭니다. 다음은 테스트 함수에서 common모듈을 호출하는 예입니다.

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

바이너리 크레이트 통합 테스트

Rust는 라이브러리 크레이트만 통합 테스트를 지원합니다.

정리

Rust의 테스트 기능 코드가 변경 되었을 때 코드가 정상 동작함을 확인하는 방법을 제공합니다. 단위 테스트는 라이브러리의 여러 부분을 개별적으로 실행하고 비공개 구현 세부 정보를 테스트할 수 있습니다. 통합 테스트는 종합적으로 라이브러리와 함께 외부 코드에서 사용하는 것과 동일한 방식으로 코드를 테스트 합니다.