Communitygithub.com

Rust 테스트 패턴

단위 테스트, 통합 테스트, 비동기 테스트, 속성 기반 테스트, 모킹, 커버리지를 포함한 Rust 테스트 패턴입니다. TDD 방법론을 따릅니다. Rust 테스트(단위, 통합, 비동기, 속성 기반 또는 커버리지) 작성 시 사용하세요.

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

이 스킬은 Rust 프로젝트에서 TDD(테스트 주도 개발) 방법론을 따르며 신뢰할 수 있고 유지보수 가능한 테스트를 작성하기 위한 종합적인 패턴과 모범 사례를 제공합니다. `#[test]` 속성을 사용한 기본 단위 테스트, `rstest`를 활용한 매개변수화된 테스트, `proptest`를 사용한 속성 기반 테스트, `mockall`을 이용한 모킹, `cargo-llvm-cov`를 통한 커버리지 측정을 포함합니다. 또한 `#[tokio::test]`를 활용한 비동기 테스트, `tests/` 디렉토리를 통한 통합 테스트, Criterion을 사용한 벤치마킹도 다룹니다. 이 스킬은 Claude Code, Cursor, Codex와 같은 AI 에이전트가 Rust 코드베이스에서 RED-GREEN-REFACTOR 사이클을 안내하고, 새로운 함수나 모듈을 위한 테스트를 생성하며, 다양한 난이도의 테스트 문제를 해결하도록 안내합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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 → PASS, 그런 다음 테스트를 통과한 상태로 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
#[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           # 무시된 테스트 실행

모범 사례

권장사항:

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

금지사항:

  • Result::is_err()로 테스트할 수 있을 때 #[should_panic]을 사용하지 마세요
  • 모든 것을 모킹하지 마세요 — 가능하면 통합 테스트를 선호하세요
  • 불안정한(flaky) 테스트를 무시하지 마세요 — 수정하거나 격리하세요
  • 테스트에서 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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬