테스트 코드 작성

Rust Testing

테스트 함수

Rust에서 테스트는 함수로 구현되며, 테스트 코드를 넣기 위해서는 함수(fn) 앞에 "#[test]" attribute를 붙이면 된다. 예를 들어, 아래와 같은 코드를 작성하고, "cargo test"를 실행하면, cargo는 #[test]가 붙어 있는 함수들을 찾아 차례로 실행하게 된다.

#[test]
fn add_test() {
    assert!(1 + 1 == 2);
}

위 테스트 코드는 1+1의 결과가 2인 지를 체크하는 것으로 assert!() 매크로 안의 식이 참이면 pass이고, 거짓이면 error로 취급한다. 위 테스트를 아래와 같이 실행하면 성공을 표시할 것이다. cargo test는 그 프로젝트 안에 있는 모든 테스트 함수들을 실행한다.

$ cargo test
Compiling minigrep v0.1.0 
Running unittests (target\debug\deps\myapp-250ccc917f744b53.exe)

running 1 test
test add_test ... ok

테스트 모듈

Rust에서 테스트는 크게 Unit Test와 Integration Test로 구분하는데, 각 소스 단위를 테스트하는 유닛 테스트는 일반적으로 src 폴더 안의 각 소스파일 안에 "tests"라는 모듈로 작성하고, 여러 코드를 통합하여 테스트하는 것을 목적으로 하는 Integration Test는 별도의 파일에 작성한다. 유닛 테스트는 각 소스파일에 있는 private/public 아이템을 유닛 테스트할 때 사용하고, Integration Test는 여러 소스들의 public 아이템을 사용하여 통합된 테스트를 진행할 때 사용한다.

유닛 테스트 모듈은 아래 예제와 같이 모듈명(보통 tests 로 명명하지만 다른 명칭 가능) 앞에 #[cfg(test)] attribute를 붙이고, 그 안에 test 함수(들)을 갖는다. 테스트 모듈 안에는 보통 여러 개의 테스트 함수들을 둘 수 있으며, 필요한 경우 여러 테스트 함수들에서 호출할 수 있는 보조함수들을 넣을 수 있다.

#[cfg(test)]
mod tests {
    #[test]
    fn add_test() {
        assert!(1 + 1 == 2);
    }

    #[test]
    fn mul_test() {
        assert_eq!(2 * 3, 6);
    }
}

#[cfg(test)] attribute는 "cargo test"를 실행할 때만 컴파일되고 실행되도록 지시하는 것으로 이 섹션은 "cargo build" 등과 같은 명령에서 컴파일되지 않는다. 코드를 빌드(cargo build)할 때, 일반적으로 테스트 코드를 함께 넣을 필요가 없으며, 만약 넣게 되면 코드 사이즈가 불필요하게 커질 것이므로 이 부분을 제외하도록 일반적으로 #[cfg(test)] attribute를 사용한다.

Integration Test의 경우 일반적으로 "tests" 라는 디렉토리(src 디렉토리와 동일한 레벨)에 넣게 되며, 테스트 파일 안에 각 테스트 함수를 직접 사용하거나 테스트 모듈을 만들어 사용한다. 테스트 모듈의 경우 Unit Test와 달리 별도의 테스트 파일을 사용하기 때문에 테스트 모듈 위에 #[cfg(test)] attribute를 사용할 필요가 없다.

assert 매크로

assert!(식) 매크로는 "식"에 있는 값이 true이면 테스트가 성공한 것으로 여기고, false 이면 에러인 것으로 취급한다.

assert!() 매크로는 하나의 표현식을 받아들여 그 결과가 true인 지 false인 지만 체크하는 반면, assert_eq!() 혹은 assert_ne!() 매크로는 2개의 파라미터를 받아들여 에러가 난 경우 두 파라미터의 값이 어떻게 다른지 출력해 준다. assert_eq!() 매크로는 둘이 같은 지를 체크하고 같지 않으면 에러를 낸다. 반대로 assert_ne!() 매크로는 둘이 같지 않으면 성공하고 같으면 에러를 낸다.

예를 들어, 아래 예제는 고의로 두번째 파라미터에 6 대신 5를 넣은 것으로 이 테스트는 에러가 발생한 것이다.

#[cfg(test)]
mod tests {
    #[test]
    fn mul_test() {
        assert_eq!(2 * 3, 5);  // 에러
    }
}

테스트가 성공하면 assert!() 매크로나 assert_eq!() 매크로는 아무런 차이가 없지만, assert_eq!() 매크로는 실패하면 아래와 같이 좌, 우 파라미터가 어떻게 다른지 자세하게 출력하게 된다. Rust의 assert 매크로는 (다른 test framework과 달리) expected vs actual 의 구분이 없이, 그냥 두 파라미터를 비교한다.

    test tests::mul_test ... FAILED
    
    failures:
    
    ---- tests::mul_test stdout ----
    thread 'tests::mul_test' panicked at 'assertion failed: `(left == right)`
    left: `6`,
    right: `5`', src\lib.rs:48:9
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

만약 assert 매크로에 사용자가 정의하는 에러 메시지를 넣기 위해서는 assert!() 매크로의 경우는 첫번째 파라미터 이후에, assert_eq!(), assert_ne!() 매크로의 경우는 두번째 파라미터 이후에 format!에 전달할 수 있는 값들을 넣으면 된다. 예를 들어, 아래는 assert!() 매크로에 커스텀 메시지를 넣은 것으로, "wrong. expecting: {}", 2 와 같이 2개의 파라미터를 넣고 있다. 이는 format!() 매크로 안에 들어가는 2개의 파라미터이다.

    #[test]
    fn add_test() {
        assert!(1 + 1 == 3, "wrong. expecting: {}", 2);
    }

위 테스트를 실행하면 아래와 같이 커스텀 메시지가 출력된다.

test tests::add_test ... FAILED

failures:

---- tests::add_test stdout ----
thread 'tests::add_test' panicked at 'wrong. expecting: 2', src\lib.rs:43:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

should_panic

만약 테스트가 panic! 이 호출될 것으로 예측한다면, 그 테스트 함수 위에 #[should_panic] attribute를 넣으면 된다. 이 attribute는 테스트가 실행되었을 때, panic이 나면 성공(pass)으로 취급하게 되고, panic이 나지 않으면 에러로 취급한다.

아래 예제는 div() 함수에서 b=0 인 경우 panic! 이 발생하는데, 이를 테스트하기 위해 div_test() 테스트 함수 위에 #[should_panic] 을 넣은 것이다.

pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("b cannot be zero");
    }
    a / b 
}

#[cfg(test)]
mod tests {
    #[test]
    #[should_panic]
    fn div_test() {
        super::div(3, 0);  // panic 발생
    }
}

This site is not affiliated with or endorsed by the Rust Foundation or Rust Project.