基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/puk0806/gugbab-claude --skill testing-rust命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
DDD(Domain-Driven Design) 아키텍처 핵심 패턴 - 유비쿼터스 언어, 서브도메인, 바운디드 컨텍스트, Aggregate, Entity/VO, 도메인 서비스/이벤트, 레이어드 아키텍처
대규모 React/Next.js 프로젝트를 layer-first(types/·utils/·hooks/·api/·components/ 밑에 도메인이 반복되는 구조)에서 domain-first(feature/도메인 우선) 구조로 전환하는 설계 기준과 절차. Feature-Sliced Design 2.1 정본(layers 6종·slices·segments·import 규칙·@x 크로스임포트·public API), FSD를 쓰지 않는 경량 대안(features + shared 2~3계층 + ESLint import/no-restricted-paths), Next.js App Router 공존 전략(route group `()`·private folder `_`·colocation), Turborepo/Nx 모노레포에서 폴더↔패키지 승격 기준, colocation과 배럴 파일 성능 트레이드오프, 도메인 경계 역추출(import 그래프·change coupling·용어 클러스터), 전환 실패 패턴(shared 비대화·entities 남용·순환 의존·도메인=라우트 착각·조기 추상화). 도메인 개념 자체(바운디드 컨텍스트·유비쿼터스 언어)는 `architecture/ddd` 스킬을 참조한다.
소스 파일 수천 개 규모 프론트엔드 코드베이스를 멈추지 않고 점진 재구조화하는 실행 전략 - Strangler Fig / Branch by Abstraction / Parallel Change, ts-morph·jscodeshift codemod, PR 분할·검증 게이트·되돌리기, 테스트 없는 코드의 안전망, 작업 순서 설계와 위반 수 기반 진행 추적
| name | testing-rust |
| description | Rust 테스트 패턴 — 단위 테스트, 통합 테스트, |
소스: https://doc.rust-lang.org/book/ch11-00-testing.html | https://docs.rs/tokio/latest/tokio/attr.test.html | https://docs.rs/axum/0.8/axum/ | https://docs.rs/tower/latest/tower/trait.ServiceExt.html 검증일: 2026-06-20
주의: Rust 1.75+ / tokio 1.x / axum 0.8.x 기준으로 작성되었습니다.
같은 파일 내에 #[cfg(test)] 모듈을 만들어 비공개 함수까지 테스트할 수 있다. cargo test에서만 컴파일된다.
// src/domain/entity.rs
pub struct UserId(pub i64);
impl UserId {
pub fn is_valid(&self) -> bool {
self.0 > 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_user_id() {
assert!(UserId(1).is_valid());
}
#[test]
fn invalid_user_id() {
assert!(!UserId(0).is_valid());
assert!(!UserId(-1).is_valid());
}
}
핵심 규칙:
#[cfg(test)]는 cargo test 시에만 해당 모듈을 컴파일한다use super::*;로 부모 모듈의 비공개 항목에 접근 가능#[test] 어트리뷰트 필수#[cfg(test)]
mod tests {
#[test]
fn assert_examples() {
// 불리언 검사
assert!(true);
assert!(1 + 1 == 2);
// 동등성 검사 (Debug trait 필요)
assert_eq!(4, 2 + 2);
assert_ne!(3, 2 + 2);
// 커스텀 메시지
let result = 42;
assert_eq!(result, 42, "expected 42 but got {result}");
// 패턴 매칭 (stable Rust에서 사용 가능)
let value: Result<i32, String> = Ok(42);
assert!(matches!(value, Ok(42)));
let err: Result<i32, String> = Err("fail".to_string());
assert!(matches!(err, Err(ref msg) if msg.contains()));
}
}
주의:
std::assert_matches::assert_matches!는 nightly 전용(#![feature(assert_matches)])입니다. stable Rust에서는matches!매크로와assert!를 조합하세요.
함수가 panic하는지 검증한다.
#[cfg(test)]
mod tests {
#[test]
#[should_panic]
fn panics_on_invalid_input() {
divide(10, 0);
}
#[test]
#[should_panic(expected = "division by zero")]
fn panics_with_message() {
divide(10, 0);
}
}
? 연산자를 사용하려면 Result를 반환한다.
#[cfg(test)]
mod tests {
#[test]
fn result_test() -> Result<(), String> {
let value: i32 = "42".parse().map_err(|e| format!("{e}"))?;
assert_eq!(value, 42);
Ok(())
}
}
#[tokio::test]는 비동기 테스트 함수를 위한 tokio 매크로다. 내부적으로 tokio 런타임을 생성한다.
#[cfg(test)]
mod tests {
#[tokio::test]
async fn async_test() {
let result = some_async_fn().await;
assert_eq!(result, 42);
}
}
런타임 flavor 설정:
// current_thread (기본값) — 단일 스레드, 대부분의 단위 테스트에 적합
#[tokio::test]
async fn default_single_thread() { /* ... */ }
// multi_thread — 멀티스레드 런타임이 필요한 경우
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn multi_thread_test() { /* ... */ }
주의:
#[tokio::test]의 기본 flavor는current_thread입니다.#[tokio::main]의 기본값인multi_thread와 다릅니다.
필요 feature: macros와 rt (또는 "full")
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }
Service 계층의 비즈니스 로직을 DB 없이 테스트한다. Repository trait의 In-Memory 구현을 주입한다.
관련 스킬: repository-pattern 스킬의 InMemoryUserRepository 참조
// tests/mock_repo.rs 또는 src 내부 테스트 모듈
use std::collections::HashMap;
use std::sync::atomic::{AtomicI64, Ordering};
use tokio::sync::RwLock;
pub struct InMemoryUserRepository {
store: RwLock<HashMap<i64, User>>,
next_id: AtomicI64,
}
impl InMemoryUserRepository {
pub fn new() -> Self {
Self {
store: RwLock::new(HashMap::new()),
next_id: AtomicI64::new(1),
}
}
/// 테스트 셋업용: 초기 데이터를 미리 넣는 헬퍼
pub async fn with_seed(users: Vec<User>) -> Self {
let repo = Self::new();
let mut store = repo.store.write().await;
for user in users {
let id = repo.next_id.fetch_add(1, Ordering::SeqCst);
store.insert(id, user);
}
repo
}
}
impl UserRepository {
(&, id: &UserId) <<User>, DomainError> {
= .store.().;
(store.(&id.).())
}
(&, input: &CreateUser) <User, DomainError> {
.(&input.email).?.() {
(DomainError::(, , &input.email));
}
= .next_id.(, Ordering::SeqCst);
= User {
id: (id),
email: input.email.(),
name: input.name.(),
created_at: chrono::Utc::(),
};
= .store.().;
store.(id, user.());
(user)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> UserService<InMemoryUserRepository> {
UserService::new(InMemoryUserRepository::new())
}
#[tokio::test]
async fn create_and_get_user() {
let service = setup();
let input = CreateUser {
email: "test@example.com".to_string(),
name: "Test User".to_string(),
};
let created = service.create_user(input).await.unwrap();
assert_eq!(created.email, "test@example.com");
let found = service.get_user(created.id.clone()).await.unwrap();
assert_eq!(found.id, created.id);
}
#[tokio::test]
async fn duplicate_email_returns_conflict() {
let service = setup();
service.create_user(CreateUser {
email: "dup@example.com".(),
name: .(),
})..();
= service.(CreateUser {
email: .(),
name: .(),
}).;
(matches!(result, (DomainError::Conflict { .. })));
}
() {
= ();
= service.(()).;
(matches!(result, (DomainError::NotFound { .. })));
}
}
axum의 Router는 tower::Service를 구현한다. tower::ServiceExt::oneshot으로 HTTP 요청을 직접 보내 테스트할 수 있다.
주의: axum은 자체 TestClient를 제공하지 않습니다. 공식 테스트 패턴은
tower::ServiceExt::oneshot입니다.
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1" # body 읽기 유틸
hyper = "1"
#[cfg(test)]
mod tests {
use axum::{
body::Body,
http::{Request, StatusCode},
Router,
};
use http_body_util::BodyExt; // .collect() 메서드
use tower::ServiceExt; // .oneshot() 메서드
// 테스트용 Router 생성 (AppState에 InMemory 주입)
fn test_app() -> Router {
let repo = InMemoryUserRepository::new();
let service = UserService::new(repo);
let state = AppState {
user_service: Arc::new(service),
};
create_router_with_state(state)
}
#[tokio::test]
async fn test_get_user_not_found() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/users/999")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
() {
= ();
= app
.(
Request::()
.()
.()
.(, )
.(Body::(
serde_json::(&serde_json::json!({
: ,
:
})).(),
))
.(),
)
.
.();
(response.(), StatusCode::CREATED);
= response.().()..().();
: serde_json::Value = serde_json::(&body).();
(user[], );
}
}
oneshot은 Service를 소비(consume)한다. 여러 요청을 보내려면 매번 Router를 생성하거나 .into_service()를 사용한다.
#[tokio::test]
async fn test_multiple_requests() {
let app = test_app();
// Router를 Service로 변환하면 clone 가능
let mut app = app.into_service();
// 첫 번째 요청
let req1 = Request::builder()
.method("POST")
.uri("/users")
.header("content-type", "application/json")
.body(Body::from(r#"{"email":"a@b.com","name":"A"}"#))
.unwrap();
let res1 = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(req1)
.await
.unwrap();
assert_eq!(res1.status(), StatusCode::CREATED);
// 두 번째 요청 (같은 서비스 인스턴스)
let req2 = Request::builder()
.uri("/users/1")
.body(Body::empty())
.();
= ServiceExt::<Request<Body>>::(& app)
.
.()
.(req2)
.
.();
(res2.(), StatusCode::OK);
}
상세 레퍼런스 (예제·고급 패턴·흔한 실수) →
references/REFERENCE.md