Rust 테스팅 | 단위 테스트, 통합 테스트, 벤치마크
이 글의 핵심
cargo test로 단위·통합 테스트를 바로 돌릴 수 있어, CI에 붙이기도 좋습니다. #[test]와 #[should_panic] 등으로 실패·패닉을 명시적으로 검증할 수 있습니다.
시리즈 안내
#10 | 📋 전체 목차 | 이전: #09 웹 개발 · 다음: #11 CLI 도구
들어가며
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를 함께 돌립니다.
비교 및 대안
| 종류 | 용도 |
|---|---|
| 단위 테스트 | 순수 함수, 빠른 피드백 |
| 통합 테스트 | 바이너리 경계, 파일·네트워크 |
| 문서 테스트 | 예제 코드 동기화 |
추가 리소스
내부 동작과 핵심 메커니즘
이 글의 주제는 「Rust 테스팅 | 단위 테스트, 통합 테스트, 벤치마크」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 시스템·런타임 경계(스케줄링, I/O, 메모리, 동시성)를 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
핵심 요약
- #[test]: 테스트 함수 표시
- assert!: 조건 검증
- assert_eq!/assert_ne!: 값 비교
- #[should_panic]: panic 테스트
- 통합 테스트: tests/ 디렉토리
다음 단계
관련 글
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Rust 테스팅 fn add(a: i32, b: i32) -> i32 { a + b }. 실전 예제와 코드로 개념부터 활용까지 정리합니다. Rust·테스트·testing 중심으로 설명합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust 동시성 | Thread, Channel, Arc, Mutex
- C++ 단위 테스트 완벽 가이드 | Google Test로 배우는 TDD (실전 예제)
- Swift 에러 처리 | do-catch, throw, Result
이 글에서 다루는 키워드 (관련 검색어)
Rust, 테스트, testing, 벤치마크 등으로 검색하시면 이 글이 도움이 됩니다.