[Rust] 테스팅

9 minute read

프로그램이 기대하는 기능을 하는지 검증한다.

테스트 함수의 동작

  1. 필요한 데이터나 상태를 설정하기
  2. 테스트하고 싶은 코드를 실행하기
  3. 결과가 예상대로인지 단언하기(assert)

러스트의 테스트 함수

#[test] 어노테이션이 위에 달려있는 함수

cargo test 커맨드를 실행하면 test 속성이 달려있는 함수를 실행하고 테스트 성공 여부를 보고한다.

ex)

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

위의 테스트를 실행하면 아래와 같이 나온다.

$ cargo test

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

테스트가 1 중 1개가 통과했기에 1 passed; 0 failed;,

#[ignore] 어노테이션으로 무시한 테스트가 없기 때문에 0 ignored;,

벤치마크 테스트를 하지 않아서 0 measured; (나이틀리 러스트에서만 가능)

라고 나왔다.

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

assert! 매크로의 인자로 true가 들어가면 아무일도 일어나지 않고

false가 들어가면 panic! 매크로가 호출돼서 테스트를 실패하게 한다.

assert_eq!와 assert_ne!를 이용한 Equality 테스트

aseert_eq!는 입력되는 두 인자가 같은지 확인하고

assert_ne!는 입력 인자가 다른지 확인한다.

다른 테스트 프레임워크에서는 단언 함수의 파리미터를 expectedactual로 불러서 인자를 넣는 순서가 중요한데

aseert_eq! 매크로는 인자를 leftright로 불러서 인자 넣는 순서가 중요하지 않다.

 

assert_eq!assert_ne! 매크로는 각각 ==과 != 연산자를 이용한다.

비교되는 값들이 PartialEqDebug 트레잇을 구현해야 한다.

두 트레잇이 없으면 #[derive(PartialEq, Debug)] 어노테이션을 추가하면 된다.

커스텀 실패 메시지 추가하기

assert!, assert_eq!, assert_ne! 매크로는 필요한 인자 이후의 인자들은 format! 매크로에 넘겨진다.

아래 assert! 매크로는 테스트 실패 첫 번쨰 인자 뒤에 있는 인자들을 출력한다.

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

should_panic을 이용한 패닉에 대한 체크

테스트 함수에 #[should_panic] 어노테이션을 추가한다.

패닉이 안 일어나면 테스트에 실패한다.

#[cfg(test)]
mod tests {

    #[test]
    #[should_panic]
    fn greater_than_100() {
        panic!("Panic!!!")
    }
}

단순 패닉 발생뿐만 아니라, 패닉 메시지까지 테스트 해야 한다면

should_panic 속성에 expected 파라미터를 추가한다.

#[cfg(test)]
mod tests {

    #[test]
    #[should_panic(expected = "Panic!!!")]
    fn greater_than_100() {
        panic!("Panic!!!")
    }
}

 

테스트 실행 방식 제어하기

cargo test 바로 뒤에 나열된 옵션은 cargo test에 입력되고

-- 구분자 뒤에 나열된 옵션은 테스트 바이너리에 입력된다.

테스트를 병렬 혹은 연속으로 실행하기

테스트 실행 시, 스레드를 이용해서 병렬로 수행된다.

테스트를 빨리 끝내서 피드백을 빨리 얻기 위함이다.

테스트 간 공유하는 환경이나 자원이 있다면, 테스트 실행 시 예상하지 못한 결과가 나올 수 있다.

예를 들어, 같은 이름의 텍스트 파일을 읽는 테스트와 쓰는 테스트가 있다면 각 테스트가 서로 간섭을 일으켜서 테스트가 실패할 수 있다.

테스트를 한 번에 하나씩만 실행하게 해서 문제를 해결할 수 있다.

테스트 바이너리에 --test-threads 옵션을 줘서 사용하려는 스레드 개수를 설정할 수 있다.

다음 명령어는 스레드 개수를 1개로 지정해서, 테스트 프로그램이 병렬이 아닌 연속적으로 실행되게 한다.

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

함수 결과 보여주기

테스트 실행 시 발생하는 표준 출력은 테스트 실패 시에만 볼 수 있다.

아래 prints_and_returns_10 함수는 입력 값을 출력하고 10을 반환한다.

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

위 테스트 실행 시

테스트에 성공하는 this_test_will_pass 함수에서 발생한 표준 출력은 무시되고

테스트에 실패하는 this_test_will_fail 함수에서 발생한 표준 출력만 테스트 정리 출력에 나타난다.

--nocapture 옵션을 주면, 성공한 테스트에서 나오는 표준 출력도 볼 수 있다.

cargo test -- --nocapture

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

cargo test에 실행하고 싶은 테스트 함수 이름을 넘겨서 일부 테스트만 실행할 수 있다.

ex)

cargo test test_name

실행이 안 된 테스트는 filtered로 표시된다.

여러 개의 테스트를 실행시키기 위한 필터링

테스트 함수 이름 중 add_two_and_twoadd_three_and_two와 같이 add로 시작하는 테스트를 실행하려면 cargo test add 커맨드를 실행한다.

테스트 무시하기

실행 시간이 긴 테스트는 #[ignore] 어노테이션을 추가해서 무시할 수 있다.

cargo test -- --ignored 커맨드를 실행하면 무시된 테스트만 실행할 수 있다.

 

테스트 조직화

테스트는 크게 단위(unit) 테스트와 통합(integration) 테스트로 나눈다.

단위 테스트는 작고, 한 번에 하나의 모듈만 분리해서 테스트하고, 비공개 인터페이스를 테스트한다.

통합 테스트는 라이브러리 외부에서 공개 인터페이스를 이용해 여러 개의 모듈을 테스트한다.

단위 테스트

관례적으로 각 파일마다 테스트 함수를 담고 있는 tests 모듈을 만들고, 이 모듈에 cfg(test) 어노테이션을 붙인다.

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

#[cfg(test)] 어노테이션은 cargo build 실행 시에는 무시하고 cargo test 실행 시에만 컴파일하고 실행하라고 러스트에게 지시한다.

통합 테스트는 다른 디렉터리에 있어서 #[cfg(test)] 어노테이션이 필요없지만

단위 테스트는 해당 코드와 같이 있기에 컴파일 시 결과물 크기를 줄이기 위해 #[cfg(test)]가 필요하다.

비공개 함수 테스트하기

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 키워드가 없는 비공개 모듈이다.

tests 모듈은 같은 파일 내에 있어서 비공개 함수도 일반 함수처럼 가져와서 테스트한다.

통합 테스트

라이브러리의 공개 API 부분에 속하는 함수들만 호출해서 테스트한다.

라이브러리의 수 많은 요소들이 함께 올바르게 동작하는지 확인한다.

tests 디렉토리

프로젝트 디렉토리의 최상위, src 옆에 tests 디렉터리를 만든다.

tests 디렉터리 안에 테스트 파일을 넣어두면 Cargo가 각 파일을 개별 크레이트처럼 컴파일해서 테스트를 실행한다.

tests/integration_test.rs 파일을 만들고 아래 코드를 집어넣는다.

extern crate adder;

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

tests 디렉터리 내 각 테스트가 모두 개별적인 크레이트이기에, extern crate adder으로 라이브러리를 가져왔다.

#[cfg(test)] 어노테이션도 필요 없다.

cargo test--test test_name 옵션을 줘서 특정 통합 테스트 함수를 실행 시킬 수 있다.

ex)

cargo test --test integration_test

통합 테스트 내의 서브모듈

통합 테스트 내에서 헬퍼 함수를 공유함과 동시에 테스트 출력에 나오지 않게 하고 싶다

러스트는 tests/common/mod.rs 파일로 만든 common 모듈을 테스트 파일로 취급하지 않는다.

tests/common/mod.rs 만든 후, 아래와 같이 mod common;으로 헬퍼 모듈을 불러와 사용할 수 있다.

extern crate adder;

mod common;

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

바이너리 크레이트를 위한 통합 테스트

프로젝트가 src/lib.rs 파이링 없고 src/main.rs 파일만 가지고 있는 바이너리 프로젝트라면,

tests 디렉터리 내에 통합 테스트를 만들어도 src/main.rs에 정의된 함수를 가져오기 위해 extern crate를 사용할 수 없다.

보통 src/lib.rs에 중요한 기능들이 정의되어 있고 이는 extern crate으로 불러 와서 테스트할 수 있다.

라이브러리 내의 중요 기능들이 잘 작동한다면, src/main.rs 내에 있는 소량의 코드들은 테스트할 필요가 없다.

마무리

타입 시스템과 소유권 규칙이 몇 가지 버그를 방지해줘도

테스트는 논리 버그를 잡아서 코드가 기대한 동작을 수행한다고 검증할 수 있다.

Categories:

Updated: