Rust 테스트 전략 — 단위/통합/문서 테스트 제대로 쓰기
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
모범 사례
- 각 함수마다 최소 3개의 테스트 케이스: 정상 케이스, 경계값, 에러 케이스
- Doc test 작성: 문서화와 테스트를 동시에
- 의미 있는 테스트명: test_1 보다 test_add_positive_numbers
- 한 테스트 한 가지만: 테스트가 실패했을 때 원인이 명확해야 함
- 테스트도 코드: 테스트도 유지보수하고 리팩토링해야 함
결론
Rust의 테스트 시스템은 언어에 깊이 통합되어 있다. 단위 테스트, 통합 테스트, 문서 테스트를 조합하면 매우 견고한 코드를 작성할 수 있다. 그리고 문서가 항상 최신이라는 것이 가장 큰 이점이다.