API 테스트하기
통합 테스트
API 테스트하기은(는) CoddyKit의 무료 Learn Rust Coding 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Learn Rust Coding 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Learn Rust Coding 강의에는 총 4개의 강의가 포함되어 있습니다.
API를 테스트해야 하는 이유
테스트를 사용하면 엔드포인트가 올바르게 동작하고 코드를 변경한 뒤에도 계속 작동한다는 확신을 얻을 수 있습니다. REST API에서 가장 유용한 테스트는 통합 테스트입니다. 통합 테스트는 실제 라우터를 처음부터 끝까지 실행하면서 요청을 보내고 응답을 검증합니다.
이 레슨에서는 단위 테스트, Tower의 oneshot 기법, 전체 통합 테스트를 다룹니다.
순수 로직을 위한 단위 테스트
유효성 검사처럼 네트워크에 접근하지 않는 로직은 일반적인 Rust 단위 테스트로 테스트할 수 있습니다. 코드 옆에 #[cfg(test)] 모듈을 작성하세요. 이러한 테스트는 cargo test로 빠르게 실행됩니다.
fn validate_title(title: &str) -> bool {
!title.trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty() {
assert!(!validate_title(" "));
assert!(validate_title("buy milk"));
}
}비동기 테스트
핸들러는 비동기이므로 await하는 테스트 함수는 런타임에서 실행되어야 합니다. #[test] 대신 #[tokio::test]를 사용하세요. 이 어노테이션은 해당 테스트를 위한 Tokio 런타임을 시작하므로 .await로 비동기 코드를 호출할 수 있습니다.
async fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn adds() {
assert_eq!(add(2, 3).await, 5);
}
}네트워크 없이 테스트하기
Axum 라우터는 Tower의 Service 트레이트를 구현하므로 포트를 바인딩하지 않고도 라우터에 요청을 직접 전달할 수 있습니다. oneshot 메서드는 하나의 Request를 받아 Response를 반환합니다. 따라서 통합 테스트를 빠르고 결정론적으로 실행할 수 있습니다.
// dev-dependencies needed: tower (for ServiceExt), http-body-util
use axum::{Router, routing::get};
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt; // brings in oneshot
async fn check() {
let app = Router::new().route("/health", get(|| async { "ok" }));
let res = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}상태 코드 검증하기
대부분의 테스트가 가장 먼저 확인하는 것은 HTTP 상태입니다. 없는 리소스는 404를, 성공적인 생성은 201을, 잘못된 본문은 400을 반환해야 합니다. 해당 경로에 대한 요청을 만들고 res.status()를 예상 코드와 비교하세요.
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt;
async fn missing_returns_404(app: axum::Router) {
let res = app
.oneshot(Request::builder()
.uri("/todos/999")
.body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}응답 본문 읽기
JSON을 검증하려면 응답 본문을 바이트로 수집한 다음 역직렬화합니다. http_body_util::BodyExt::collect 도우미가 본문을 모으고, 이어서 serde_json이 이를 모델로 파싱하므로 필드 단위로 검증할 수 있습니다.
use http_body_util::BodyExt;
async fn read_json(res: axum::http::Response<axum::body::Body>) {
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let todo: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(todo["done"], false);
}JSON 본문 보내기
POST를 테스트하려면 JSON 본문과 올바른 콘텐츠 유형 헤더가 포함된 요청을 만드세요. 입력 구조체를 직렬화하고 content-type: application/json을 설정한 다음 oneshot을 통해 전달합니다.
use axum::http::{Request, StatusCode, header};
use axum::body::Body;
use tower::ServiceExt;
async fn create_returns_201(app: axum::Router) {
let body = serde_json::json!({ "title": "test" }).to_string();
let res = app
.oneshot(Request::builder()
.method("POST").uri("/todos")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body)).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
}재사용 가능한 테스트 앱 빌더
각 테스트는 깨끗한 상태에서 시작해야 합니다. 새로운 메모리 내 데이터베이스나 테스트 데이터베이스와 함께 새로운 라우터를 만드는 도우미를 작성하세요. 모든 테스트에서 이 도우미를 호출하면 테스트가 서로 격리되어 한 테스트가 다른 테스트에 영향을 줄 수 없습니다.
use axum::Router;
use std::sync::{Arc, Mutex};
fn test_app() -> Router {
let store = Arc::new(Mutex::new(Vec::new()));
build(store) // same build() the real server uses
}tests 디렉터리
통합 테스트는 최상위 tests/ 폴더에 둡니다. 이 폴더의 각 파일은 라이브러리의 공개 API를 사용하는 별도의 크레이트로 컴파일됩니다. 따라서 실제 사용자가 보는 것과 같은 인터페이스를 통해 테스트하게 됩니다.
tests/api.rs에는 엔드포인트 테스트를 작성합니다.cargo test로 모든 테스트를 실행합니다.
// tests/api.rs
use my_api::build_router; // exported from lib.rs
#[tokio::test]
async fn health_ok() {
let _app = build_router();
// send a request and assert ...
}데이터베이스를 대상으로 테스트하기
핸들러가 sqlx를 사용한다면 테스트에도 데이터베이스가 필요합니다. 일반적인 방법으로는 전용 테스트 데이터베이스, 각 테스트가 끝난 뒤 롤백하는 트랜잭션, 또는 테스트마다 깨끗한 데이터베이스를 자동으로 준비하는 sqlx의 #[sqlx::test] 매크로가 있습니다.
// requires sqlx test features and a DATABASE_URL
use sqlx::PgPool;
#[sqlx::test]
async fn inserts_todo(pool: PgPool) {
let todo = create(&pool, "learn rust").await.unwrap();
assert_eq!(todo.title, "learn rust");
assert_eq!(todo.done, false);
}무엇을 테스트할까요
균형 잡힌 테스트 모음을 목표로 하세요:
- 정상 경로: 유효한 요청이 올바른 상태와 본문을 반환하는지 확인합니다.
- 오류: 없는 리소스에는 404가, 잘못된 입력에는 400이 반환되는지 확인합니다.
- 경계 사례: 빈 목록, 경계값, 중복을 확인합니다.
배포 전에 회귀를 발견할 수 있도록 CI에서 cargo test를 실행하세요.
빠른 확인
API 테스트에 대한 이해도를 확인해 보세요.
복습
Rust REST API를 테스트하는 방법을 배웠습니다:
- 순수 로직은 단위 테스트하고, 비동기 코드에는
#[tokio::test]를 사용합니다. oneshot은 포트 없이 라우터를 직접 실행합니다.- 본문과 헤더가 있는 요청을 만들고, 응답 본문을 수집하고 파싱합니다.
- 격리를 위해 테스트마다 새로운 앱 빌더를 사용하고, 통합 테스트는
tests/에 둡니다. #[sqlx::test]는 깨끗한 데이터베이스를 준비하며, 정상 경로와 오류, 경계 사례를 모두 다룹니다.
자주 묻는 질문
“API 테스트하기” 강의는 무료인가요?
네 — “API 테스트하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Learn Rust Coding 강의 전체를 잠금 해제할 수 있습니다. Learn Rust Coding 강의에는 총 4개의 강의가 포함되어 있습니다.
“API 테스트하기”에서 뭘 배우나요?
통합 테스트 브라우저에서 직접 실행하는 실습 코드로 Learn Rust Coding을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Learn Rust Coding을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Learn Rust Coding은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“API 테스트하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Learn Rust Coding 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Learn Rust Coding 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.