Communitygithub.com

Rust 테스팅 패턴

유닛 테스트, 통합 테스트, 비동기 테스트, 프로퍼티 기반 테스트, 모킹, 커버리지를 포함한 Rust 테스팅 패턴입니다. TDD 방법론을 따릅니다. Rust 테스트(유닛, 통합, 비동기, 프로퍼티 기반, 커버리지)를 작성할 때 사용하세요.

Rust 테스팅 패턴란 무엇인가요?

이 스킬은 Rust 프로젝트에서 신뢰할 수 있고 유지보수 가능한 테스트를 작성하기 위한 포괄적인 가이드입니다. 유닛 테스트, 통합 테스트, 비동기 테스트(Tokio), 프로퍼티 기반 테스트(proptest), 모킹(mockall), 벤치마킹(criterion), 문서 테스트, 커버리지(cargo-llvm-cov) 등 다양한 테스팅 패턴을 다룹니다. TDD(Red-Green-Refactor) 워크플로우를 강조하며, 각 단계별 코드 예제와 함께 설명합니다. 주요 기능으로는 모듈 수준 테스트 구성, 매개변수화된 테스트(rstest), 사용자 정의 전략, CI 통합(GitHub Actions) 예제를 포함합니다. Claude Code, Cursor, Codex 등 모든 AI 에이전트 플랫폼과 호환되며, Rust 개발자가 테스트 커버리지를 높이고 코드 품질을 향상시키는 데 도움을 줍니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/rust-testing

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Rust 테스팅 패턴은(는) 무엇을 하나요?

TDD 방법론을 따라 신뢰할 수 있고 유지보수 가능한 테스트를 작성하기 위한 포괄적인 Rust 테스팅 패턴입니다.

사용 시기

  • 새로운 Rust 함수, 메서드 또는 트레이트를 작성할 때
  • 기존 코드에 테스트 커버리지를 추가할 때
  • 성능이 중요한 코드에 대한 벤치마크를 생성할 때
  • 입력 유효성 검사를 위한 프로퍼티 기반 테스트를 구현할 때
  • Rust 프로젝트에서 TDD 워크플로우를 따를 때

작동 방식

  1. 대상 코드 식별 — 테스트할 함수, 트레이트 또는 모듈을 찾습니다
  2. 테스트 작성 — #[cfg(test)] 모듈 내에서 #[test], 매개변수화된 테스트를 위한 rstest, 또는 프로퍼티 기반 테스트를 위한 proptest를 사용합니다
  3. 의존성 모킹 — mockall을 사용하여 테스트 대상을 격리합니다
  4. 테스트 실행 (RED) — 예상된 오류로 테스트가 실패하는지 확인합니다
  5. 구현 (GREEN) — 테스트를 통과하는 최소한의 코드를 작성합니다
  6. 리팩토링 — 테스트를 통과한 상태로 코드를 개선합니다
  7. 커버리지 확인 — cargo-llvm-cov를 사용하여 80% 이상을 목표로 합니다

Rust를 위한 TDD 워크플로우

RED-GREEN-REFACTOR 주기

RED     → 먼저 실패하는 테스트를 작성합니다
GREEN   → 테스트를 통과하는 최소한의 코드를 작성합니다
REFACTOR → 테스트를 통과한 상태로 코드를 개선합니다
REPEAT  → 다음 요구사항으로 계속 진행합니다

Rust에서 단계별 TDD

// 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 → 통과, 그런 다음 테스트를 통과한 상태로 REFACTOR합니다

유닛 테스트

모듈 수준 테스트 구성

// 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!("유효하지 않은 이메일: {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", "[email protected]").unwrap();
        assert_eq!(user.display_name(), "Alice");
        assert_eq!(user.email, "[email protected]");
    }

    #[test]
    fn rejects_invalid_email() {
        let result = User::new("Bob", "not-an-email");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("유효하지 않은 이메일"));
    }
}

단언 매크로

assert_eq!(2 + 2, 4);                                    // 동등
assert_ne!(2 + 2, 5);                                    // 부등
assert!(vec![1, 2, 3].contains(&2));                     // 부울
assert_eq!(value, 42, "42가 예상되었지만 {value}를 얻었습니다"); // 사용자 정의 메시지
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON);   // 부동소수점 비교

오류 및 패닉 테스트

Result 반환 테스트

#[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 = "인덱스가 범위를 벗어났습니다")]
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 사용

#[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(), "시간 초과되어야 합니다");
}

테스트 구성 패턴

rstest를 사용한 매개변수화된 테스트

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);
}

// 픽스처
#[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");
    }
}

proptest를 사용한 프로퍼티 기반 테스트

기본 프로퍼티 테스트

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());
    }
}

mockall을 사용한 모킹

트레이트 기반 모킹

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());
}

문서 테스트

실행 가능한 문서

/// 두 숫자를 더합니다.
///
/// # 예제
///
/// ```
/// 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
}

/// 설정 문자열을 파싱합니다.
///
/// # 오류
///
/// 입력이 유효한 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!()
}

Criterion을 사용한 벤치마킹

# 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 (또는 CI에서 taiki-e/install-action 사용)
cargo llvm-cov                    # 요약
cargo llvm-cov --html             # HTML 보고서
cargo llvm-cov --lcov > lcov.info # CI용 LCOV 형식
cargo llvm-cov --fail-under-lines 80  # 임계값 미만이면 실패

커버리지 목표

코드 유형목표
핵심 비즈니스 로직100%
공개 API90%+
일반 코드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           # 무시된 테스트 실행

모범 사례

DO (권장):

  • 테스트를 먼저 작성하세요 (TDD)
  • 유닛 테스트에는 #[cfg(test)] 모듈을 사용하세요
  • 구현이 아닌 동작을 테스트하세요
  • 시나리오를 설명하는 설명적인 테스트 이름을 사용하세요
  • 더 나은 오류 메시지를 위해 assert!보다 assert_eq!를 선호하세요
  • 더 깔끔한 오류 출력을 위해 Result를 반환하는 테스트에서 ?를 사용하세요
  • 테스트를 독립적으로 유지하세요 — 공유 가능한 가변 상태를 피하세요

DON'T (권장하지 않음):

  • Result::is_err()로 테스트할 수 있는 경우 #[should_panic]을 사용하지 마세요
  • 모든 것을 모킹하지 마세요 — 가능할 때는 통합 테스트를 선호하세요
  • 불안정한 테스트를 무시하지 마세요 — 수정하거나 격리하세요
  • 테스트에서 sleep()을 사용하지 마세요 — 채널, 배리어 또는 tokio::time::pause()를 사용하세요
  • 오류 경로 테스트를 건너뛰지 마세요

CI 통합

# GitHub Actions
test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: clippy, rustfmt

    - name: 포맷 검사
      run: cargo fmt --check

    - name: Clippy
      run: cargo clippy -- -D warnings

    - name: 테스트 실행
      run: cargo test

    - uses: taiki-e/install-action@cargo-llvm-cov

    - name: 커버리지
      run: cargo llvm-cov --fail-under-lines 80

기억하세요: 테스트는 문서입니다. 코드가 어떻게 사용되어야 하는지 보여줍니다. 명확하게 작성하고 최신 상태로 유지하세요.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬