with one click
testing-rust
Rust 테스트 패턴 — 단위 테스트, 통합 테스트,
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Rust 테스트 패턴 — 단위 테스트, 통합 테스트,
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| 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("fail")));
}
}
주의:
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 for InMemoryUserRepository {
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
let store = self.store.read().await;
Ok(store.get(&id.0).cloned())
}
async fn create(&self, input: &CreateUser) -> Result<User, DomainError> {
if self.find_by_email(&input.email).await?.is_some() {
return Err(DomainError::conflict("user", "email", &input.email));
}
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let user = User {
id: UserId(id),
email: input.email.clone(),
name: input.name.clone(),
created_at: chrono::Utc::now(),
};
let mut store = self.store.write().await;
store.insert(id, user.clone());
Ok(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".to_string(),
name: "User 1".to_string(),
}).await.unwrap();
let result = service.create_user(CreateUser {
email: "dup@example.com".to_string(),
name: "User 2".to_string(),
}).await;
assert!(matches!(result, Err(DomainError::Conflict { .. })));
}
#[tokio::test]
async fn get_nonexistent_returns_not_found() {
let service = setup();
let result = service.get_user(UserId(999)).await;
assert!(matches!(result, Err(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);
}
#[tokio::test]
async fn test_create_user() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/users")
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_string(&serde_json::json!({
"email": "test@example.com",
"name": "Test User"
})).unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
// 응답 본문 읽기
let body = response.into_body().collect().await.unwrap().to_bytes();
let user: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(user["email"], "test@example.com");
}
}
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())
.unwrap();
let res2 = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(req2)
.await
.unwrap();
assert_eq!(res2.status(), StatusCode::OK);
}
상세 레퍼런스 (예제·고급 패턴·흔한 실수) →
references/REFERENCE.md
Spring Security 5.5.x + jjwt 0.10.7 레거시 JWT 인증 - WebSecurityConfigurerAdapter, OncePerRequestFilter, javax.servlet 환경
Spring Boot 3.x + Spring Security 6.x + jjwt 0.12.x 기반 모던 JWT 인증 패턴. SecurityFilterChain Bean, 람다 DSL, jakarta.servlet, Virtual Threads 적용
Unity 6 LTS 2D 모바일 게임용 uGUI 시스템 전문 스킬. Canvas/RectTransform/TextMeshPro, 모바일 UI 패턴(팝업·무한 스크롤·광고·IAP), 성능 최적화, UI Toolkit과의 선택 기준 포함.
아크라시아(akrasia, 자제력 없음) 학술 논쟁의 핵심 구도와 주요 연구자·문헌을 빠르게 파악할 수 있는 도메인 지식 스킬. 도덕윤리교육 전공 대학원생(석/박사)이 학위논문·KCI 투고·세미나 준비 시 고대–현대–한국 학계–도덕심리학 흐름을 한 번에 짚도록 구성. <example>사용자: "아리스토텔레스의 propeteia와 astheneia 구분을 인용하려는데 출처를 알려줘"</example> <example>사용자: "데이비슨이 의지박약을 어떻게 가능하다고 봤는지 핵심 논증을 정리해줘"</example> <example>사용자: "한국 도덕교육 학계에서 아크라시아 다룬 논문 있어?"</example>
아리스토텔레스 『니코마코스 윤리학』에서 akrasia(자제력없음)와 akolasia(무절제)의 5축 차이를 정밀하게 정리한 학위논문 자료 스킬. NE VII.4 1147b20-1148b14, VII.8 1150b29-1151a28, III.10-12 1117b23-1119b18 절별 분해와 표준 학자 해석(Bostock, Broadie-Rowe, Pakaluk, Hursthouse, Charles 등)을 포함. 도덕교육 적용을 위한 두 상태 차이의 함의 및 한국어 번역어 처리 권장안 제공. <example>사용자: "akrates와 akolastos를 prohairesis 측면에서 어떻게 구분해야 하나요?"</example> <example>사용자: "NE VII.4의 ἁπλῶς akrasia가 akolasia와 어떻게 갈라지는지 절별 분해해주세요"</example> <example>사용자: "Hursthouse의 연속체 모델을 도덕교육 적용 절에서 어떻게 활용할 수 있나요?"</example>
한국 위기 대응 자원(자살·자해·정신건강·여성·청소년·노인·다문화) 핫라인과 앱·챗봇 안전 가드 응답 패턴 종합. 꿈 해몽·정신건강 앱 등 자가 진단/감정 콘텐츠 도메인에서 위험 신호 포착 시 안전한 자원 안내 문구를 작성할 때 참조. <example>사용자: "꿈 해몽 앱에 위기 안내 문구를 어떻게 넣을까?"</example> <example>사용자: "한국에서 자살예방 핫라인 번호가 어떻게 바뀌었지?"</example> <example>사용자: "정신건강 챗봇 안전 가드 응답 템플릿을 짜줘"</example>