From ecc
단위 테스트, 통합 테스트, 비동기 테스트, 속성 기반 테스트, 모킹(mocking) 및 커버리지를 포함한 러스트 테스트 패턴입니다. TDD 방법론을 따릅니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
TDD 방법론을 따라 신뢰할 수 있고 유지보수가 용이한 테스트를 작성하기 위한 포괄적인 러스트 테스트 패턴입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
TDD 방법론을 따라 신뢰할 수 있고 유지보수가 용이한 테스트를 작성하기 위한 포괄적인 러스트 테스트 패턴입니다.
#[cfg(test)] 모듈 내에서 #[test]를 사용하거나, 파라미터화된 테스트를 위해 rstest를, 속성 기반 테스트를 위해 proptest를 사용합니다.mockall을 사용하여 테스트 대상을 격리합니다.cargo-llvm-cov를 사용하며, 80% 이상을 목표로 합니다.RED → 실패하는 테스트를 먼저 작성
GREEN → 테스트를 통과시키는 최소한의 코드 작성
REFACTOR → 테스트 통과를 유지하며 코드 개선
REPEAT → 다음 요구 사항으로 이동
// RED: 테스트를 먼저 작성하고, todo!()를 플레이스홀더로 사용
pub fn add(a: i32, b: i32) -> i32 { todo!() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() { assert_eq!(add(2, 3), 5); }
}
// cargo test → 'not yet implemented' 패닉 발생
// GREEN: todo!()를 최소한의 구현으로 대체
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → 통과(PASS), 이후 테스트를 유지하며 리팩터링 수행
// src/user.rs
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
let email = email.into();
if !email.contains('@') {
return Err(format!("invalid email: {email}"));
}
Ok(Self { name: name.into(), email })
}
pub fn display_name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.display_name(), "Alice");
assert_eq!(user.email, "alice@example.com");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid email"));
}
}
assert_eq!(2 + 2, 4); // 일치 여부
assert_ne!(2 + 2, 5); // 불일치 여부
assert!(vec![1, 2, 3].contains(&2)); // 불리언 조건
assert_eq!(value, 42, "expected 42 but got {value}"); // 커스텀 메시지
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // 부동 소수점 비교
#[test]
fn parse_returns_error_for_invalid_input() {
let result = parse_config("}{invalid");
assert!(result.is_err());
// 특정 에러 배리언트 확인
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::ParseError(_)));
}
#[test]
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
let config = parse_config(r#"{"port": 8080}"#)?;
assert_eq!(config.port, 8080);
Ok(()) // ? 연산자가 Err를 반환하면 테스트 실패
}
#[test]
#[should_panic]
fn panics_on_empty_input() {
process(&[]);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_specific_message() {
let v: Vec<i32> = vec![];
let _ = v[0];
}
my_crate/
├── src/
│ └── lib.rs
├── tests/ # 통합 테스트 디렉토리
│ ├── api_test.rs # 각 파일은 별도의 테스트 바이너리로 컴파일됨
│ ├── db_test.rs
│ └── common/ # 공유 테스트 유틸리티
│ └── mod.rs
// tests/api_test.rs
use my_crate::{App, Config};
#[test]
fn full_request_lifecycle() {
let config = Config::test_default();
let app = App::new(config);
let response = app.handle_request("/health");
assert_eq!(response.status, 200);
assert_eq!(response.body, "OK");
}
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().items.len(), 3);
}
#[tokio::test]
async fn handles_timeout() {
use std::time::Duration;
let result = tokio::time::timeout(
Duration::from_millis(100),
slow_operation(),
).await;
assert!(result.is_err(), "should have timed out");
}
use rstest::{rstest, fixture};
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
// 픽스처(Fixtures)
#[fixture]
fn test_db() -> TestDb {
TestDb::new_in_memory()
}
#[rstest]
fn test_insert(test_db: TestDb) {
test_db.insert("key", "value");
assert_eq!(test_db.get("key"), Some("value".into()));
}
#[cfg(test)]
mod tests {
use super::*;
/// 적절한 기본값으로 테스트 사용자를 생성합니다.
fn make_user(name: &str) -> User {
User::new(name, &format!("{name}@test.com")).unwrap()
}
#[test]
fn user_display() {
let user = make_user("alice");
assert_eq!(user.display_name(), "alice");
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(input in ".*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded);
}
#[test]
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
let original_len = vec.len();
vec.sort();
assert_eq!(vec.len(), original_len);
}
#[test]
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
vec.sort();
for window in vec.windows(2) {
assert!(window[0] <= window[1]);
}
}
}
use proptest::prelude::*;
fn valid_email() -> impl Strategy<Value = String> {
("[a-z]{1,10}", "[a-z]{1,5}")
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
}
proptest! {
#[test]
fn accepts_valid_emails(email in valid_email()) {
assert!(User::new("Test", &email).is_ok());
}
}
use mockall::{automock, predicate::eq};
#[automock]
trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
fn save(&self, user: &User) -> Result<(), StorageError>;
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn service_returns_none_when_not_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.returning(|_| None);
let service = UserService::new(Box::new(mock));
assert!(service.get_user(99).is_none());
}
/// 두 숫자를 더합니다.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 설정 문자열을 파싱합니다.
///
/// # Errors
///
/// 입력이 유효한 TOML이 아닌 경우 `Err`를 반환합니다.
///
/// ```no_run
/// use my_crate::parse_config;
///
/// let config = parse_config(r#"port = 8080"#).unwrap();
/// assert_eq!(config.port, 8080);
/// ```
///
/// ```no_run
/// use my_crate::parse_config;
///
/// assert!(parse_config("}{invalid").is_err());
/// ```
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
todo!()
}
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "benchmark"
harness = false
// benches/benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
# 설치: cargo install cargo-llvm-cov
cargo llvm-cov # 요약 보고서
cargo llvm-cov --html # HTML 보고서 생성
cargo llvm-cov --lcov > lcov.info # CI용 LCOV 형식
cargo llvm-cov --fail-under-lines 80 # 기준 미달 시 실패 처리
| 코드 유형 | 목표 |
|---|---|
| 핵심 비즈니스 로직 | 100% |
| 공개 API | 90% 이상 |
| 일반 코드 | 80% 이상 |
| 생성된 코드 / FFI 바인딩 | 제외 |
cargo test # 모든 테스트 실행
cargo test -- --nocapture # println 출력 표시
cargo test test_name # 패턴과 일치하는 테스트 실행
cargo test --lib # 단위 테스트만 실행
cargo test --test api_test # 특정 통합 테스트만 실행
cargo test --doc # 문서 테스트만 실행
cargo test --no-fail-fast # 첫 실패 시 멈추지 않고 계속 실행
cargo test -- --ignored # 무시된(ignored) 테스트 실행
권장 사항:
#[cfg(test)] 모듈을 사용하세요assert!보다 assert_eq!를 선호하세요Result를 반환하는 테스트에서는 ? 연산자를 사용하여 깔끔하게 작성하세요금지 사항:
Result::is_err()를 테스트할 수 있다면 #[should_panic] 사용을 지양하세요sleep()을 사용하지 마세요 — 채널, 배리어(barriers) 또는 tokio::time::pause()를 사용하세요# GitHub Actions 예시
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage
run: cargo llvm-cov --fail-under-lines 80
기억하세요: 테스트는 곧 문서입니다. 당신의 코드가 어떻게 사용되어야 하는지 보여줍니다. 명확하게 작성하고 항상 최신 상태로 유지하세요.