Rust에서 테스트(test) 작성하기

test의 assert_eq!

Rust는 안정성과 신뢰성을 강조하는 언어답게 테스트를 매우 중요하게 여깁니다. 표준 라이브러리에는 테스트를 쉽게 작성하고 실행할 수 있도록 test 프레임워크가 기본 내장되어 있으며, cargo test 명령어로 손쉽게 실행할 수 있습니다. 오늘은 단위 테스트, 통합 테스트, 그리고 테스트 관련 어노테이션 및 모범 사례에 대해 알아보겠습니다.


1. 기본적인 단위 테스트 구조

Rust의 테스트는 일반적으로 소스 파일 내에 다음과 같은 구조로 작성됩니다.

// 실제 함수
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

// 테스트 모듈
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
  • #[cfg(test)]: 이 모듈은 테스트 시에만 컴파일되도록 지정합니다.
  • mod tests { … }: 테스트용 하위 모듈을 의미합니다. 실제 코드와 분리된 테스트 코드입니다.
  • use super::*;: 상위 모듈(super)의 add 함수를 불러오는 것입니다. 이 줄을 주석처리하면 “add함수가 없다”고 합니다.
  • #[test]: 테스트 함수임을 표시합니다.
  • test_add 함수를 만들어서 add(2, 3)의 결과와 5가 같은지 비교해서 맞으면 ok, 다르면 failed가 발생합니다.
    assert_eq!: 기대값과 실제값이 같은지 검사합니다.

가. 실행 방법

cargo test

cargo는 프로젝트 전체를 빌드하고 #[test] 함수들을 자동으로 실행합니다.

나. lib.rs 파일 작성 및 실행

(1) assert_eq!의 결과 두 값이 같은 경우

src 폴더 아래에 lib.rs란 파일로 작성했습니다.

Rust의 test 모듈 작성 및 실행 방법

day21 폴더에서 cargo test를 실행하니(VS Code를 보면 함수 위에 Run Test가 있으므로 이걸 눌러도 됨)
컴파일되고 실행되는데 먼저 unittests src\lib.rs라고 표시되어 lib.rs의 test annotation을 먼저 실행하고, 그 다음은 main.rs를 실행합니다.

lib.rs의 테스트 결과는
test tests::test_add … ok라고 표시되고,
그 아래 test result: ok. 1 passed; 0 failed… 등 내용이 표시되는데,

main.rs의 테스트 결과는 test 어노테이션이 없어서 0으로 표시됩니다.

그리고, assert_eq! 구문만 실행되고, main.rs에 있는 println!(“Hello, world!”);문은 실행되지 않습니다. lib.rs에 println!문이 있어도 실행되지 않습니다.

cargo test –lib라고 하면 lib.rs의 test annotation만 실행하고, main.rs에 test 어노테이션이 있는지 체크하지 않습니다.

(2) assert_eq!의 결과 두 값이 다른 경우

asserteq!의 실패시 panic 및 살패에 대한 설명문

add(2, 3)의 결과는 5인데, 6과 비교하면 두 값이 다르므로 FAILED라고 결과가 표시되고,

그 아래를 보면 thread ‘tests::test_add’ panicked at day21\src\lib.rs:12:9:라고 panic이 발생했고,
왼쪽은 5, 오른쪽은 6으로 “왼쪽과 오른쪽이 같다는 단언(주장)이 실패했다”고 설명합니다.


2. 다양한 assert 매크로

Rust에서는 다음과 같은 다양한 테스트 도구를 제공합니다.

매크로설명
assert!조건이 true인지 확인
assert_eq!두 값이 같은지 확인
assert_ne!두 값이 다른지 확인(not equal)
dbg!디버깅 용도로 값을 출력
panic!강제로 실패시키기

예제:

#[test]
fn test_assertions() {
let a = 10;
let b = 20;
assert!(a < b);
assert_eq!(a + b, 30);
assert_ne!(a, b);
}

위 코드를 cargo test –lib로 실행하면
모든 결과가 True이므로 test result: ok인데, 세 번이 아니라 한 번만 표시되고,

cargo test --lib

assert!(a > b);
assert_eq!(a + b, 40);
으로 수정해서 2개를 False로 만든 후 실행하면
a > b에서 panic이 되기 때문에 a + b와 40을 비교하는 부분은 가지도 못하고 끝납니다.

assert!가 실패하면 panic이 되므로 그 다음 assert_eq!를 실행하지 못함

따라서, a < b로 수정하고 실행하면 a + b = 40에서 실패가 발생합니다.


3. 실패하는 테스트

테스트가 실패했을 경우에는 어떤 테스트가 어떤 이유로 실패했는지 친절히 알려줍니다. 예를 들어:

#[test]
fn test_fail() {
assert_eq!(1 + 1, 3); // 실패
}

실행 시 다음과 같은 메시지가 출력됩니다.
“왼쪽은 2인데, 오른쪽은 3으로 왼쪽과 오른쪽이 같지 않다”고 합니다.

---- test_fail stdout ----
thread 'test_fail' panicked at day21\src\lib.rs:27:5:
assertion `left == right` failed
left: 2
right: 3

4. 테스트 함수에서 panic이 발생하면?

Rust에서는 테스트 함수가 panic을 일으키면 테스트가 실패한 것으로 간주됩니다. 일부러 panic을 유발하는 테스트는 다음처럼 작성할 수 있습니다:

#[test]
#[should_panic]
fn test_panic() {
panic!("강제로 실패시킴");
}
  • #[should_panic]: 이테스트는 panic이 발생해야 성공으로 간주됩니다.
테스트 함수에서 panic이 발생하면 : 
#[should_panic]: panic이 발생해야 ok

5. 결과 반환형을 이용한 테스트

테스트 함수가 Result<(), E>를 반환할 수도 있습니다.

#[test]
fn test_with_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("'2 + 2는 4'가 아닙니다!"))
}
}

가. Ok(성공)인 경우

successes:
test_with_result
라고 test_with_result가 성공임을 표시합니다.

Result를 이용한 결괏값 반환

나. 에러인 경우

failures에 Error: “‘2 + 2는 4’가 아닙니다!”라고 표시됩니다.

Result 결괏값이 Err인 경우

이 방식은 ? 연산자를 활용할 수 있어 더 깔끔하게 작성할 수 있습니다.

다. ? 연산자를 이용한 구문

fn check_math() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("'2 + 2는 4'가 아닙니다!"))
}
}

#[test]
fn test_with_result() -> Result<(), String> {
check_math()?; // 실패하면 즉시 반환됨
Ok(())
}

6. 테스트 분류

가. 단위 테스트 (Unit Test)

  • 하나의 함수나 모듈의 기능을 검사
  • 일반적으로 동일한 파일 내 mod tests 안에 작성

나. 통합 테스트 (Integration Test)

  • 실제 사용 시나리오처럼 여러 모듈이 함께 작동하는지를 테스트
  • tests/ 디렉터리 하위에 별도의 .rs 파일로 작성

예:

my_project/
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

// tests/integration_test.rs
use my_project::add;

#[test]
fn test_add_integration() {
assert_eq!(add(10, 20), 30);
}

위 integration_test.rs의 use문에 my_project가 있는데, day21 등으로 프로젝트명에 맞게 수정해야 합니다.

cargo test를 실행하면 src폴더의 lib.rs와 main.rs뿐만 아니라, tests폴더의 integration_test.rs까지 test가 있는지 찾아서 test를 진행하고, day21 폴더의 Doc-tests도 진행합니다.

통합 테스트 - doctest 포함

그러나, tests 폴더의 integration_test.rs에서 Run Test를 누르면
integration_test.rs의 test만 실행합니다.

vs code의 Run Test 명령

7. #[ignore]와 특정 테스트만 실행하기

테스트가 너무 오래 걸리거나 특별한 조건에서만 실행하고 싶을 때는 #[ignore] 어노테이션을 붙일 수 있습니다.

#[test]
#[ignore]
fn long_running_test() {
// 오래 걸리는 테스트
}

cargo test 실행 시 무시되며,
test long_running_test … ignored

#[ignore] - 테스트시 제외

아래와 같이 실행하면 #[ignore]가 붙은 테스트만 실행합니다.

cargo test -- --ignored

위 코드를 실행하면 1 passed; … 1 filtered out;이라고 표시되는데, long_running_test는 통과되고, test_add_integration은 제외되었다는 뜻입니다.

#[ignore]가 붙은 테스트만 실행

특정 테스트만 실행하려면 이름을 지정합니다:

cargo test test_add

8. 테스트 모범 사례

  • 작고 독립적인 테스트를 작성하세요.
  • 각 테스트는 부작용이 없어야 합니다 (예: 파일 시스템 접근, DB 쓰기 등).
  • 테스트 함수 이름은 명확하게 지어야 합니다(test_add, test_divide_by_zero 등).
  • 테스트는 문서화 주석과 함께 유지하면 가독성이 높아집니다.

9. 문서화 테스트 (doctest)

Rust는 문서 주석(///) 안에 포함된 코드도 테스트로 실행합니다.

/// 두 수를 더합니다.
///
/// # 예시
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

cargo test를 실행하면 이 문서 예제도 자동으로 테스트합니다.

my_crate는 프로젝트명에 맞게 수정해야 하는데, day21 프로젝트이므로 day21로 수정했습니다.

위에서는 Doc-tests시 test 개수가 0이었는데, 여기는 1로 표시되고, ‘add (line 48) … ok’라고 표시됩니다.

doctest

10. 마무리

Rust의 테스트 프레임워크는 매우 강력하면서도 간단하게 사용할 수 있습니다. 실수를 미리 잡아내고 코드를 문서화하며 안정성을 높이기 위해 테스트는 필수적인 도구입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다