비욘드 JS: 러스트 - test codes

dante Yoon·2022년 12월 30일
0

beyond js

목록 보기
12/20
post-thumbnail

글을 시작하며

TDD가 대중화 되며 테스트 작성 방법은 코드를 작성하는 사람이 필수로 알아야 하는 기본 소양이 되었습니다.
러스트에서 테스트를 작성하는 방법을 찬찬히 알아보도록 하겠습니다.

테스트를 작성하는 과정

  1. 테스트 코드에 사용할 상태값과 데이터를 작성
  2. 테스트를 실행
  3. 기대값과 산출값의 비교

테스트 함수 분석

러스트에서 테스트 코드를 작성하는 가장 쉬운 방법은 함수에 테스트 함수임을 나타네는 annotation을 붙이는 것입니다.
그리고 테스트를 실행하기 위해서는 cargo test 명령어를 입력합니다.

Cargo를 사용해 라이브러리 모듈을 만들면 테스트 함수와 테스트 모듈이 자동으로 생성됩니다.

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

lib.rs에 자동으로 생성된 테스트 코드

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

테스트 코드를 실행해보자

#[test] annotation은 이 함수가 테스트 함수임을 나타내어 test runner가 테스트 코드를 수행할 수 있게 합니다.

assert_eq!매크로는 result의 값을 검증하는데 사용합니다.
테스트를 돌려보기 위해서 cargo test를 터미널에 입력합니다.

테스트 함수 이름을 변경해보자

테스트 함수 이름을 exploration으로 변경해보고 다시 실행해보겠습니다.

테스트를 실패해보자

mod tests {
    use super::*;

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

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

another 함수 내부에 panic! 매크로를 작성해 일부러 패닉을 발생시킵니다.

결과값을 검증하자

표준 라이브러리에서 제공하는 assert! 매크로를 사용해서 값을 검증할 수 있습니다. argument로 불린 타입을 넣어야 합니다.

struct Rectangle의 can_hold 함수에 로직을 작성한 후 두 Rectangle 간의 너비 및 높이를 비교하겠습니다.


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

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

impl Rectangle은 tests 모듈의 outer scope에 작성되었기 때문에 inner module인 tests 모듈 내부에서 사용하기 위해서 suer::*를 사용했습니다.

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

코드 변경점을 테스트 해보기 위해 임의로 can_hold 내부의 로직을 올바르지 않게 수정했습니다.
너비의 조건을 > 에서 <로 바꾸었습니다.

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 src/lib.rs (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! 매크로는 테스트 실행 결과만 알려주고 argument에 사용된 값을 출력해주지는 않습니다.

positive는 assert_eq!, negative는 assert_ne!를 사용하여 콘솔에 값을 출력해줄 수 있습니다.

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

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

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (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'

콘솔에 출력되는 것을 확인하기 위해 일부러 틀린 함수 add_two를 작성했습니다. left, right에 대한 값이 콘솔에 출력되는 것을 볼 수 있습니다.

assert_eq!는 == 연산을, assert_ne!는 !=를 사용합니다.
콘솔에 argument를 출력하기 때문에 argument는 PartialEq, Debug trait을 구현해야 합니다.

커스텀 실패 메세지를 작성하자.

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

  #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{}`",
            result
        );
    }

greeting의 반환 값은 "Hello!" 입니다.

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

thread 'main' panicked at 'Greeting did not contain name, value was Hello!', src/lib.rs:12:9

panic을 테스트 하는 should_panic

아래코드에서 greeter_than100 함수 바디에 Guess 인스턴스 선언에 있어 100이 넘어가는 값을 넣었기 떄문에 panic이 발생하는 것이 정상입니다. 아래 코드에서 #[should_panic] attribute를 작성하여 패닉이 발생해야 하는 케이스를 확인해보겠습니다.

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

패닉의 메시지를 확인

패닉마다 발생시키는 조건이 다를 경우 다음처럼 shgould_panic 속성에 메세지를 넘겨 특정 패닉을 구체화하여 테스트 할 수 있습니다.

아래 코드에서 greater_than_100 함수는 less than or equal to 100 메세지가 포함된 패닉이 검출될 경우 테스트를 통과 시킵니다.

pub struct Guess {
    value: i32,
}
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);
    }
}

위 코드는 정상적으로 통과해야 합니다. 만약 메세지가 다르다면 아래처럼 에러가 발생합니다.

Result<T, E>를 사용하기

현재까지 함수가 정상작동을 싪패할 시 항상 panic을 발생시켜 검출했습니다. 이번에는 패닉을 대신해 Result<T,E> 를 사용하는 방법을 알아보겠습니다.

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

위 코드에서는 assert_eq! 매크로를 사용하지 않았습니다. Ok, Err varaint를 사용해 테스트 코드의 성공과 실패 분기를 나누었습니다.

테스트 옵션

cargo run 명령어를 통해 컴파일된 바이너리 코드를 실행시키는 것 같이 cargo test는 컴파일된 코드를 테스트 모드에서 실행할 수 있게 해줍니다.

cargo test의 디폴트 실행 모드는 테스트를 병렬로 실행하는 것입니다. 여기에 옵션을 추가할 경우 어떻게 실행 환경을 변경할 수 있는지 알아보겠습니다.

-- seperator (구분자)

옵션을 주기 위해 CLI(command line interface)에서 테스트 명령어와 옵션을 분리하여 작성할 수 있게 -- 구분자를 사용합니다. cargo test -- --help는 테스트와 연관된 옵션들을 커맨드 창에 출력합니다.

테스트 병렬 / 독립 실행

여러 개의 테스트를 빠르게 수행하기 위해 러스트 테스트모드는 여러 개의 스레드를 사용하여 병렬로 각 테스트를 실행시킵니다. 여러 개의 테스트가 동시에 실행되기 때문에 각 테스트에서 사용되는 데이터나 상태, 환경변수가 서로 다른 테스트에 영향을 미치지 않게 주의를 요해야 합니다.

예를 들어 test-output.txt에 수정을 가하고 이를 읽는 테스트를 수행한다면 두 테스트 간의 로직 상에서 충돌이 일어나, 코드를 정확히 작성했더라도 실행된 순서에 따라 테스트가 성공하거나 실패하는 일이 발생할 것입니다.

위와 같은 상황에서 동일한 파일을 사용해서 테스트를 수행할 경우 테스트를 병렬로 실행하는 것을 방지하기 위해 --test-threads 옵션을 사용해 테스트 수행에 사용할 스레드의 갯수를 통제할 수 있습니다.

cargo test -- --test-threads=1

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


정상 실행했을 때의 println! 매크로 I got the value 4는 출력되지 않았습니다.
모든 테스트의 결과를 콘솔 창에 출력하고 싶다면 다음 처럼 옵션을 줍니다.

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

단독 실행

앞서 작성한 테스트 함수 중 one_hundred만 선택하여 실행하겠습니다.

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [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

몇 개의 테스트만 실행하기

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [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

이름을 명시하여 해당 테스트 이름이 포함되는 테스트 스위트만 실행했습니다.

전체 테스트 중 몇 개의 테스트를 무시하기

테스트 성격에 따라 시간이 오래걸리는 테스크의 경우 해당 테스트를 실행 범위에서 제외할 수 있습니다.
다음의 코드에서 #[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 src/lib.rs (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.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

앞서 ignore 속성 derive 로 설정한 테스트들은 ignored 리스트로 별도 필터링됩니다. 이 리스트를 실행하고 싶으면 다음과 같이 실행시킬 수 있습니다.

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [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

ignored 리스트에 있는 테스트를 포함하여 실행하고 싶다면 다음 처럼 --include-ignored 옵션을 사용합니다.

cargo test -- --include-ignored

테스트 용어 정리

테스트에 사용하는 용어가 너무 다양하여 여러 테스트 사용자에게 혼란을 주기 때문에 러스트 커뮤니티는 여러 파편화된 용어를 두 가지 메인 카테고리로 분리하여 관리하기로 했습니다.

Unit test

단일 모듈을 분리하여 실행하거나 private interface를 테스트하는 경우

Integration test

다른 라이브러리 모듈을 사용하는 것과 동일하게 public interface만을 사용하여 하나의 테스트마다
여러 개의 모듈을 동시에 사용하여 테스트하는 경우

유닛 테스트

유닛 테스트는 각 함수의 주요 로직의 정합성만을 타겟으로 빠르게 실행하는 테스트입니다. src 디렉토리 안의 각 파일에 분리하여 작성해 유닛테스트를 실행시킬 수 있습니다. 테스트 작성에 있어 컨벤션은 각 파일에 tests로 모듈 이름을 작성한 후 각 모듈에 cfg(test) 어노테이션을 붙이는 것입니다.

테스트 모듈과 #[cfg(test)] 어노테이션

테스트 모듈의 #[cfg(test)] 어노테이션은 러스트에게 cargo test를 터미널에 입력했을 때만 테스트를 수행하라고 이야기해줍니다. 따라서 cargo build를 입력했을 때는 실행되지 않습니다.
이를 통해 러스트는 빌드된 컴파일 아티팩트의 사이즈와 컴파일 시간을 줄일 수 있습니다.

곧 살펴볼 통합 테스트와의 차이점으로, 통합 테스트는 별도의 디렉토리에 작성되기 때문에 #[cfg(test)] 어노테이션이 필요하지 않습니다. 하지만 유닛 테스트는 라이브러리 코드와 동일한 디렉토리에 작성되기 떄문에 빌드 범위에서 제외하기 위해 #[cfg(test)]를 명시해야 합니다.

테스트 함수 분석 섹션에서 adder 라이브러리 모듈을 생성했었습니다.

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

이후 자동으로 생성되는 테스트 보일러플레이트 코드는 아래와 같았습니다.

// src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

여기서 cfg는 attribute(속성)이라고 하며 configuration의 줄임말입니다. 이 속성은 러스트에게 특정 옵션이 주어졌을 때만 컴파일 대상에 포함되어야 한다고 말을 합니다. congiruation option이 test이기 때문에 러스트 컴파일러는 이 테스트코드를 실행시킵니다. cfg 속성 덕분에 Cargo는 cargo test 명령어를 수행할 때만 테스트를 실행하게 됩니다.

test private function

private 함수를 테스트 대상 디렉토리에 작성해야 하는지는 테스트 커뮤니티에서 의견이 분분한 주제입니다. 다른 언어와 다르게 러스트의 Privacy 규칙은 private 함수를 실행시킬 수 있게 합니다. internal_adder private 함수를 살펴보겠습니다.

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

internal_adder 함수는 pub 키워드와 함께 작성되지 않았습니다. 테스트는 그저 러스트 코드이고 테스트 모듈은 그저 다른 모듈입니다. child 모듈에 있는 아이템들은 ancestor 모듈의 아이템을 사용할 수 있습니다. 위 테스트에서 use super::*를 사용해 상위 모듈의 아이템들을 tests 모듈로 가져왔습니다. 그 덕분에 테스트는internal_adder를 호출할 수 있게 되었습니다.

private 함수를 실행하고 싶지 않다고 해서 러스트는 아무런 강제를 가하지 않습니다.

통합 테스트(intergration test)

통합 테스트 코드는는 작성한 라이브러리의 외부 디렉토리에 존재합니다.
이 테스트의 목적은 작성한 라이브러리 코드가 다른 외부 환경의 코드와 잘 융합하는지 검증하는 것이기 때문에 다른 코드와 동일하게 외부에서 라이브러리 코드를 가져다 사용합니다. 외부에 있기 때문에 통합 테스트를 작성할 때는 라이브러리에 public api로 명시된 코드만 가져다가 사용할 수 있습니다.

통합 테스트를 작성하기 위해서는 tests 디렉토리를 만들어야 합니다.
기존에 만들었었던 adder 모듈을 그대로 재사용하겠습니다.
tests 디렉토리를 src와 동일한 레벨에 만들었습니다.

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

tests 디렉토리에 있는 각각에 파일은 각각의 별도 crate이며 우리는 라이브러리를 각 테스트 crate 스코프에 포함시켜야 합니다. 이를 위해 유닛 테스트와 다르게 코드 최상단에 use adder를 작성했습니다.

유닛 테스트와 다르게 cfg annotation을 작성하지 않은 이유는 cargo가 tests 디렉토리를 별도의 특별한 디렉토리로 취급하여 cargo test를 수행했을 때만 실행시키기 때문입니다.

use adder;

#[test]
fn it_adds_two() {
  assert_eq!(4, adder::add_two(2));
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (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

아까 작성한 유닛테스트를 포함하여 실행되었습니다. 테스트중 하나의 스위트만 실패하더라도 이어지는 실행 예정 테스트들은 수행되지 않습니다. 유닛 테스트가 실패하면 통합 테스트가 실행되지 않습니다.
모든 유닛테스트가 성공한 직후에야 통합 테스트가 실행됩니다.

각 통합 테스트 파일은 별도의 테스트 섹션을 가지고 있다고 말합니다. 그래서 각 테스트 섹션이 실행될 때 Running tests/integration_test.rs 와 같이 콘솔에 출력됩니다.

통합 테스트를 별도로 실행하고 싶다면 --test argument와 함께 테스트 함수의 이름을 명시합니다.

$ 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

통합 테스트의 서브 모듈

리엑트에서 testing/library를 사용할 때 필요한 유틸 함수를 별도의 디렉토리로 넣어두는 것과 같이 tests 디렉토리에 다른 파일을 집어 넣고 싶을 때 서브모듈을 작성할 수있습니다.

tests/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 src/lib.rs (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

Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) 와 같이 출력되는 것을 확인할 수 있습니다. 이 파일이 테스트 코드를 포함하고 있지 않더라도 테스트 범위에 추가되는 것입니다.

이러한 상황을 방지하기 위해 tests/common.rs파일대신에 tests/common/mod.rs를 작성했습니다. 이렇게 이름을 작성함으로 인해 rust는 common 모듈을 통합 테스트로 인식하지 않습니다. 앞서 작성한 setup 함수를 tests/common.rs에서 삭제하고 tests/common/mod.rs에 작성함으로 인해 콘솔에서 유틸 함수에 대한 출력이 사라졌습니다. tests 디렉토리 내부에 있는 서브 디렉토리들은 별도의 crate으로 컴파일되지 않고 테스트 섹션도 갖지 않습니다.

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

작성한 setup 함수를 사용하기 위해 통합 테스트 코드 파일에서 해당 모듈을 불러옵니다.
mod common을 작성한 이후 commmon::setup() 함수를 호출했습니다.

// tests/integration_test.rs
use adder;

mod common;

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

binary crate를 위한 통합 테스트

binary crate이라면 lib.rs가 아니라 src/main.rs에 파일만 존재할 것입니다. src/main.rs에 작성되는 함수를 별도 디렉토리 tests에서 불러올 수 없습니다. library crate만 다른 crate에서 사용할 수 있는 함수를 외부로 노출할 수 있습니다.

이러한 이유 떄문에 main.rs에 있는 로직이 src/lib.rs에 있는 로직을 불러서 사용하는 패턴이 일반화되었습니다. 이러한 구조를 사용하면 통합 테스트 파일에서 앞서 봤던 다른 예제와 동일하게 use 를 사용해 중요한 기능을 수행하는 crate를 불러올 수 있습니다. 이렇게 하면 src/main.rs 에는 적은량의 코드만 존재해도 됩니다.

예를 들어 화씨를 섭시 온도로 바꾸어주는 CLI 어플리케이션을 만들기 위해 convert_temperature 이라는 함수를 src/lib.rs 파일에 작성한다고 합시다. src/main.rs 파일은 커맨드 라인에 입력되는 argument를 파싱하고 convert_temperature 함수를 호출하기만 합니다.

// src/lib.rs

pub fn convert_temperature(fahrenheit: f64) -> f64 {
    (fahrenheit - 32.0) * (5.0/9.0)
}

// src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let fahrenheit: f64 = args[1].parse().unwrap();
    let celsius = convert_temperature(fahrenheit);
    println!("{}°F is {}°C", fahrenheit, celsius);
}

우리는 tests 디렉토리에 convert_temperature 함수를 테스트하기 위한 코드를 작성할 수 있습니다.
이 테스트 코드에서는 use statement를 사용해 테스트 파일 스코프 내부에 함수를 불러올 수 있습니다.

// tests/temperature_conversion.rs

use crate::convert_temperature;

#[test]
fn test_freezing_point() {
    assert_eq!(convert_temperature(32.0), 0.0);
}

#[test]
fn test_boiling_point() {
    assert_eq!(convert_temperature(212.0), 100.0);
}

#[test]
fn test_negative_temperatures() {
    assert_eq!(convert_temperature(-40.0), -40.0);
}

src/lib.rs 파일은 어플리케이션의 주요 로직을 가지고 있습니다. 이 파일은 src/main.rs에서 사용할 수 있는 함수를 가지고 있습니다. 주요 로직의 위치를 main.rs에서 lib.rs로 이동하는 것만으로 use 키워드를 사용할 수 있게 되었습니다.

글을 마치며

오늘은 러스트 테스트 코드 작성 방법에 대해 알아보았습니다.
알찬 내용이 많으니 한번 복습해보시고 영상을 통해서도 한번 들어보시기 바랍니다.

감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글