Rust 테스트 전략 — 단위/통합/문서 테스트 제대로 쓰기

게시일: 2025년 12월 9일 · 14분 읽기

Rust의 doc test는 혁신이다. 문서와 테스트가 동시에 최신 상태를 유지한다. 다른 언어에서는 이게 불가능하지만, Rust에서는 기본이다. 이 글에서는 Rust의 테스트 시스템을 완전히 마스터하는 방법을 설명한다.

단위 테스트 — 같은 파일에서

Rust의 관례는 테스트 모듈을 같은 파일에 작성하는 것이다.


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

pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
        assert_eq!(add(0, 0), 0);
    }

    #[test]
    fn test_multiply() {
        assert_eq!(multiply(2, 3), 6);
        assert_eq!(multiply(-2, 3), -6);
    }

    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("이 테스트는 panic이 예상된다");
    }
}

#[cfg(test)]는 컴파일 플래그로, cargo test 실행 시에만 컴파일된다. 프로덕션 바이너리에 포함되지 않는다.

테스트 실행


# 모든 테스트 실행
cargo test

# 특정 테스트 실행
cargo test test_add

# 테스트를 병렬이 아닌 순차적으로 실행
cargo test -- --test-threads=1

# 출력 표시 (기본적으로 passed 테스트의 출력은 숨겨짐)
cargo test -- --nocapture

# 무시된 테스트 실행
cargo test -- --ignored

# 모든 테스트 (무시된 것도 포함)
cargo test -- --include-ignored

통합 테스트 — tests/ 디렉토리

큰 기능은 통합 테스트로 테스트하는 것이 좋다. 이는 tests/ 디렉토리에 작성한다.


// tests/integration_test.rs
use my_crate::*;

#[test]
fn it_can_add_numbers() {
    assert_eq!(add(2, 3), 5);
}

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

#[test]
fn workflow_test() {
    // 실제 사용 흐름을 테스트
    let a = add(1, 2);  // 3
    let b = multiply(a, 2);  // 6
    assert_eq!(b, 6);
}

tests/ 디렉토리의 각 파일은 별도의 바이너리로 컴파일되므로, 라이브러리의 public API만 테스트할 수 있다.

문서 테스트 — Doc Tests

Rust의 문서화 코드는 자동으로 실행된다. 문서가 최신임을 보장한다.


/// 두 수를 더한다.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 팩토리얼을 계산한다.
///
/// # Panics
///
/// 음수가 입력되면 panic한다.
///
/// # Examples
///
/// ```
/// use my_crate::factorial;
///
/// assert_eq!(factorial(5), 120);
/// assert_eq!(factorial(0), 1);
/// ```
///
/// 음수 입력 시:
///
/// ```should_panic
/// use my_crate::factorial;
/// factorial(-1);
/// ```
pub fn factorial(n: u32) -> u32 {
    match n {
        0 | 1 => 1,
        _ => n * factorial(n - 1)
    }
}

cargo test를 실행하면 이 예제 코드들이 모두 실행된다.


# 문서 테스트만 실행
cargo test --doc

# 특정 함수의 문서 테스트만
cargo test --doc add

테스트 Fixtures

복잡한 테스트는 공통 설정이 필요하다.


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

    struct TestContext {
        user: User,
        database: Database
    }

    fn setup() -> TestContext {
        let user = User::new("John", "john@example.com");
        let database = Database::new();
        TestContext { user, database }
    }

    #[test]
    fn test_user_creation() {
        let ctx = setup();
        assert_eq!(ctx.user.name, "John");
    }

    #[test]
    fn test_user_email() {
        let ctx = setup();
        assert_eq!(ctx.user.email, "john@example.com");
    }
}

Property-Based Testing

proptest 라이브러리를 사용하면 자동으로 다양한 입력값을 시도한다.


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

    #[test]
    fn prop_add_is_commutative() {
        proptest!(|(a in -1000i32..1000, b in -1000i32..1000)| {
            // a + b == b + a 임을 여러 값으로 증명
            assert_eq!(add(a, b), add(b, a));
        });
    }

    #[test]
    fn prop_add_with_zero() {
        proptest!(|(a in -1000i32..1000)| {
            assert_eq!(add(a, 0), a);
        });
    }
}

테스트 조직화

큰 프로젝트에서는 src/ 안에 tests 모듈을 만들기도 한다.


// src/lib.rs
pub mod math;
pub mod utils;

#[cfg(test)]
mod tests;

// src/tests/mod.rs
mod math_tests;
mod utils_tests;
mod integration_tests;

// src/tests/math_tests.rs
#[cfg(test)]
mod tests {
    use crate::math::*;

    #[test]
    fn test_add() { ... }
}

무시된 테스트

느린 테스트는 무시할 수 있다.


#[test]
#[ignore]
fn expensive_test() {
    // 이 테스트는 cargo test 시 실행되지 않음
    // cargo test -- --ignored 로만 실행됨
}

#[test]
#[ignore = "데이터베이스가 필요함"]
fn database_test() {
    // 무시 이유도 기록 가능
}

벤치마크 테스트

Rust는 벤치마크 테스트도 지원한다 (nightly 필요).


#![feature(test)]
extern crate test;

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

    #[bench]
    fn bench_add(b: &mut Bencher) {
        b.iter(|| add(2, 3));
    }

    #[bench]
    fn bench_multiply(b: &mut Bencher) {
        b.iter(|| multiply(2, 3));
    }
}

# 벤치마크 실행
cargo +nightly bench

커버리지 측정


# tarpaulin으로 커버리지 측정
cargo tarpaulin --out Html

# 또는 llvm-cov
cargo llvm-cov --html

모범 사례

결론

Rust의 테스트 시스템은 언어에 깊이 통합되어 있다. 단위 테스트, 통합 테스트, 문서 테스트를 조합하면 매우 견고한 코드를 작성할 수 있다. 그리고 문서가 항상 최신이라는 것이 가장 큰 이점이다.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.