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/CD와 C++ 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 --locked와cargo clippy -- -D warnings**를 함께 돌립니다.
비교 및 대안
| 종류 | 용도 |
|---|---|
| 단위 테스트 | 순수 함수, 빠른 피드백 |
| 통합 테스트 | 바이너리 경계, 파일·네트워크 |
| 문서 테스트 | 예제 코드 동기화 |
추가 리소스
정리
핵심 요약
- #[test]: 테스트 함수 표시
- assert!: 조건 검증
- assert_eq!/assert_ne!: 값 비교
- #[should_panic]: panic 테스트
- 통합 테스트: tests/ 디렉토리
다음 단계
- Rust CLI 도구
- Rust C++ 비교
관련 글
- Rust 시작하기 | 메모리 안전한 시스템 프로그래밍 언어