ワンクリックで
testing-rust
Rust 테스트 패턴 — 단위 테스트, 통합 테스트,
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Rust 테스트 패턴 — 단위 테스트, 통합 테스트,
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| 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>