Rust 테스팅 | 단위 테스트, 통합 테스트, 벤치마크

Rust 테스팅 | 단위 테스트, 통합 테스트, 벤치마크

이 글의 핵심

Rust 테스팅에 대해 정리한 개발 블로그 글입니다. fn add(a: i32, b: i32) -> i32 { a + b }

들어가며

cargo test로 단위·통합 테스트를 바로 돌릴 수 있어, CI에 붙이기도 좋습니다. #[test]#[should_panic] 등으로 실패·패닉을 명시적으로 검증할 수 있습니다.

단위 테스트는 모든 언어에서 중요합니다. Python에서 pytest·CI, Node.js의 Jest, C++의 Google Test, Go의 go test는 각각의 생태계에서 표준에 가깝습니다. CI/CD·컨테이너 배포와 연결하려면 Node.js GitHub Actions CI/CDC++ Docker·배포 이미지를 함께 보세요.


1. 단위 테스트 (Unit Tests)

기본 테스트

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

fn subtract(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(-2, 3), 1);
        assert_eq!(add(0, 0), 0);
    }
    
    #[test]
    fn test_subtract() {
        assert_eq!(subtract(10, 3), 7);
        assert_eq!(subtract(5, 10), -5);
    }
}

테스트 실행

# 모든 테스트 실행
cargo test

# 특정 테스트만 실행
cargo test test_add

# 출력 보기
cargo test -- --nocapture

# 단일 스레드로 실행
cargo test -- --test-threads=1

2. assert 매크로

기본 assert

#[cfg(test)]
mod tests {
    #[test]
    fn test_assertions() {
        // assert!: 조건이 true인지 확인
        assert!(true);
        assert!(2 + 2 == 4);
        
        // assert_eq!: 두 값이 같은지
        assert_eq!(2 + 2, 4);
        assert_eq!("hello".to_uppercase(), "HELLO");
        
        // assert_ne!: 두 값이 다른지
        assert_ne!(2 + 2, 5);
    }
    
    #[test]
    fn test_with_message() {
        let x = 10;
        assert!(x > 5, "x는 5보다 커야 함, 실제: {}", x);
        assert_eq!(x, 10, "x는 10이어야 함");
    }
}

should_panic

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("0으로 나눌 수 없음");
    }
    a / b
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    #[should_panic]
    fn test_divide_by_zero() {
        divide(10, 0);
    }
    
    #[test]
    #[should_panic(expected = "0으로 나눌 수 없음")]
    fn test_divide_panic_message() {
        divide(10, 0);
    }
}

Result를 반환하는 테스트

#[cfg(test)]
mod tests {
    #[test]
    fn test_with_result() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("계산 오류"))
        }
    }
}

3. 통합 테스트 (Integration Tests)

tests/ 디렉토리

// tests/integration_test.rs
use my_crate;

#[test]
fn test_add() {
    let result = my_crate::add(2, 3);
    assert_eq!(result, 5);
}

#[test]
fn test_subtract() {
    let result = my_crate::subtract(10, 3);
    assert_eq!(result, 7);
}

공통 모듈

// tests/common/mod.rs
pub fn setup() {
    // 테스트 초기화 코드
}

// tests/integration_test.rs
mod common;

#[test]
fn test_with_setup() {
    common::setup();
    // 테스트 코드
}

4. 실전 예제

예제: 계산기 테스트

struct Calculator;

impl Calculator {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    fn divide(a: i32, b: i32) -> Result<i32, String> {
        if b == 0 {
            Err(String::from("0으로 나눌 수 없음"))
        } else {
            Ok(a / b)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        assert_eq!(Calculator::add(2, 3), 5);
        assert_eq!(Calculator::add(-2, 3), 1);
    }
    
    #[test]
    fn test_divide_success() {
        assert_eq!(Calculator::divide(10, 2), Ok(5));
    }
    
    #[test]
    fn test_divide_by_zero() {
        assert!(Calculator::divide(10, 0).is_err());
    }
}

실전 심화 보강

실전 예제: 임시 디렉터리와 assert_fs 스타일 통합 테스트

표준 라이브러리만으로 파일 시스템을 건드리는 함수를 검증하는 최소 예제입니다. tempfile 크레이트를 쓰면 더 간결합니다.

Cargo.toml (dev-dependencies):

[dev-dependencies]
tempfile = "3"
use std::fs;
use std::path::Path;

fn merge_logs(dir: &Path) -> std::io::Result<String> {
    let mut out = String::new();
    let mut entries: Vec<_> = fs::read_dir(dir)?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().map(|x| x == "log").unwrap_or(false))
        .collect();
    entries.sort();
    for p in entries {
        out.push_str(&fs::read_to_string(&p)?);
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn orders_files_lexicographically() -> std::io::Result<()> {
        let dir = tempdir()?;
        let a = dir.path().join("b.log");
        let b = dir.path().join("a.log");
        fs::write(&a, "second\n")?;
        fs::write(&b, "first\n")?;
        let merged = merge_logs(dir.path())?;
        assert_eq!(merged, "first\nsecond\n");
        Ok(())
    }
}

자주 하는 실수

  • 전역 상태(환경 변수, 현재 디렉터리)에 의존한 테스트가 순서에 따라 실패하는 경우.
  • #[should_panic]만 걸고 메시지를 검증하지 않아 잘못된 panic에도 통과하는 경우.
  • **통합 테스트에서 use crate::**가 아니라 크레이트 이름으로만 import해야 하는 규칙을 혼동하는 경우.

주의사항

  • cargo test기본적으로 병렬입니다. 공유 자원이 있으면 Mutex 또는 --test-threads=1을 고려하세요.
  • 벤치마크criterion 등으로 분리해 릴리스 프로파일에서 측정합니다.

실무에서는 이렇게

  • 프로퍼티 기반 테스트(proptest)로 입력 공간을 넓혀 엣지 케이스를 잡습니다.
  • CI에서는 **cargo test --lockedcargo clippy -- -D warnings**를 함께 돌립니다.

비교 및 대안

종류용도
단위 테스트순수 함수, 빠른 피드백
통합 테스트바이너리 경계, 파일·네트워크
문서 테스트예제 코드 동기화

추가 리소스


정리

핵심 요약

  1. #[test]: 테스트 함수 표시
  2. assert!: 조건 검증
  3. assert_eq!/assert_ne!: 값 비교
  4. #[should_panic]: panic 테스트
  5. 통합 테스트: tests/ 디렉토리

다음 단계

  • Rust CLI 도구
  • Rust C++ 비교

관련 글

  • Rust 시작하기 | 메모리 안전한 시스템 프로그래밍 언어