Rust를 사용하면 수백 줄 안에 빠르고 타입 안정적인 HTTP 서버를 만들 수 있습니다. 하지만 이 서버를 테스트하기 위한 빠른 피드백 루프는 제공하지 않습니다. 컴파일 주기가 길고, cargo test는 단일 트레이트 변경에도 모든 것을 다시 실행하며, 대부분의 Rust HTTP 프레임워크는 각 엔드포인트를 한 번 호출하기도 전에 별도의 통합 테스트를 작성하도록 요구합니다. API를 출시하고 단순히 바이너리만 제공하는 것이 아니라면, Rust 툴체인 외부에 존재하며 실행 중인 서버와 통신하는 도구가 필요합니다.
이 가이드는 Apidog 내에서 완전한 Rust API 테스트 워크플로우를 안내합니다. Axum 또는 Actix 서버에 Apidog를 연결하고, 엔드포인트에 대한 요청을 만들고, Serde 직렬화된 JSON을 검증하고, JWT 인증을 처리하고, 프런트엔드가 핸들러를 완료하는 동안 개발을 진행할 수 있도록 엔드포인트를 모킹하며, 전체 과정을 CI 테스트 시나리오로 패키징하는 과정을 다룹니다. 이 가이드를 마치면 cargo build --release가 완료되기 전에 계약 변경을 감지하는 재사용 가능한 Apidog 프로젝트를 갖게 될 것입니다.
Postman 또는 curl 워크플로우를 사용하던 분들도 Apidog의 디자인 우선 기능을 무료로 이용할 수 있습니다: 저장된 요청에서 생성된 OpenAPI 스펙, 공유 가능한 모의 URL, 그리고 팀 환경. Postman 마이그레이션 이야기는 별도로 읽어보시고, 이 게시물은 Rust에 집중합니다.
요약 (TL;DR)
- Rust 서버를 로컬에서 실행하고 (Axum 또는 Actix-web 프로젝트에 대해
cargo run), 기본 URLhttp://localhost:3000을 Apidog 환경으로 추가한 다음, 모든 비밀 정보는 변수로 저장합니다. GET /healthz에 대한 첫 번째 요청을 작성하고 저장한 다음, 폴더 내의 모든 핸들러에서 인증 및 기본 URL을 재사용합니다.- JSON 엔드포인트의 경우, Serde 구조체를 Apidog의 본문 편집기에 붙여넣고 모든 전송 후에 실행되는 테스트 스크립트로 응답 형식을 단언합니다.
- 보호된 경로의 경우, JWT를 한 번 생성하여
{{token}}으로 저장하고, 폴더 수준에서 Bearer 인증을 적용하여 모든 테스트가 이를 상속받도록 합니다. - 프런트엔드 팀이 핸들러가 깔끔하게 컴파일되기 전에 실제 응답을 렌더링할 수 있도록 Apidog에서 미완성 Rust 핸들러를 모킹합니다.
- 작업 세트를 테스트 시나리오로 저장하고, 내보내고,
apidog-cli로 CI에서 실행합니다. 이제 핸들러에 영향을 미치는 모든 PR은 병합 전에 계약 검사를 받게 됩니다.
Rust 툴체인 외부에서 Rust API를 테스트하는 이유
cargo test는 훌륭합니다. 하지만 느리고, 비Rust 동료에게는 불투명하며, HTTP가 아닌 코드 중심으로 구축되어 있습니다. 핸들러가 올바른 상태 코드, 올바른 JSON 형식, 올바른 헤더, 그리고 입력이 잘못되었을 때 올바른 오류 메시지를 반환하는지 확인하려면 각 경우에 대해 새로운 tower::ServiceExt::oneshot 호출을 작성해야 합니다. 그런 다음 핸들러가 변경될 때마다 해당 테스트를 유지 관리해야 합니다. 그리고 프런트엔드가 모의를 호출할 수 있도록 JavaScript로 다시 작성해야 합니다.
Apidog는 실행 중인 서버 위에 단일 계약 계층을 제공합니다. 요청은 한 번만 존재합니다. 단언문은 요청 옆에 있습니다. 프런트엔드 동료들은 동일한 프로젝트를 열고 당신과 동일한 요청을 볼 수 있습니다. 지금으로부터 3주 후에 Serde가 #[serde(rename_all = "camelCase")] 속성을 받게 될 때, 깨지는 테스트는 프로덕션에 배포되는 테스트가 아니라 Apidog의 테스트입니다.
Rust 워크플로우에 Apidog를 추가해야 하는 세 가지 구체적인 이유:
- 계약 검사는 빌드와 분리됩니다. Apidog는 실행 중인 바이너리에 대해 실행됩니다. 엔드포인트가 여전히
200을 반환하는지 확인하기 위해rustc를 기다릴 필요가 없습니다. - 모의는 공유 가능합니다. 다른 시간대에 있는 프런트엔드 개발자는 "핸들러가 아직 완료되지 않았습니다"라는 Slack 메시지 대신 올바른 JSON을 반환하는 URL을 받습니다.
- 무료 OpenAPI. Apidog는 저장된 요청에서 OpenAPI 3.1 문서를 생성할 수 있습니다. 모든 경로에
utoipa또는aide어노테이션을 작성할 필요 없이 타입이 지정된 클라이언트를 원하는 누구에게나 이를 제공할 수 있습니다.
1단계: Rust 서버를 Apidog 환경으로 추가
Rust API를 시작합니다. Axum 프로젝트의 보일러플레이트는 다음과 같습니다:
use axum::{routing::get, Router};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let app = Router::new().route("/healthz", get(|| async { "ok" }));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Apidog를 열고 새 프로젝트를 만든 다음, 환경 관리 (우측 상단 드롭다운)를 열고 Rust Local이라는 환경을 추가합니다:
| 변수 | 값 |
|---|---|
baseUrl |
http://localhost:3000 |
token |
지금은 비워두기 |
apiVersion |
v1 |
배포된 기본 URL로 Rust Staging이라는 두 번째 환경을 추가합니다. Apidog는 환경별로 변수를 범위 지정하므로, 드롭다운 한 번 클릭으로 로컬에서 스테이징으로 전환할 수 있습니다. 저장된 요청을 통해 찾아 바꾸기를 할 필요가 없습니다.
2단계: 첫 번째 엔드포인트 호출
프로젝트 내에 Rust API라는 폴더를 만든 다음, 새 요청을 생성합니다:
- 메서드:
GET - URL:
{{baseUrl}}/healthz
전송을 누릅니다. 서버가 실행 중이면 본문이 ok인 200 응답을 받습니다. 이를 health-check로 저장합니다. 이것은 가장 간단한 스모크 테스트이며, 더 흥미로운 것을 작성하기 전에 환경과 기본 URL이 작동하는지 확인합니다.
연결 거부 오류가 발생하면 서버가 0.0.0.0에 바인딩되지 않았거나 포트가 잘못된 것입니다. Rust의 기본 TcpListener::bind("127.0.0.1:3000")는 다른 인터페이스의 localhost로 확인되는 모든 요청을 거부합니다. Apidog 및 Docker 컨테이너가 접근할 수 있도록 로컬 개발을 위해 0.0.0.0에 바인딩해야 합니다.
3단계: Serde로 JSON 요청 및 응답 테스트
가장 일반적인 Rust API 형태는 Serde 구조체로 지원되는 JSON 입력, JSON 출력 핸들러입니다. POST /users 경로를 추가합니다:
use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User { id: 1, name: payload.name, email: payload.email })
}
let app = Router::new().route("/users", post(create_user));
Apidog에서 요청을 생성합니다:
- 메서드:
POST - URL:
{{baseUrl}}/users - 본문 (JSON):
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
전송합니다. User JSON이 반환됩니다. 이를 create-user로 저장합니다.
이제 테스트 탭을 열고 단언문을 추가합니다:
pm.test("Status is 200", () => {
pm.expect(pm.response.code).to.eql(200);
});
pm.test("Body has id, name, email", () => {
const body = pm.response.json();
pm.expect(body).to.have.property("id");
pm.expect(body.name).to.eql("Ada Lovelace");
pm.expect(body.email).to.match(/^[^@]+@[^@]+$/);
});
다음에 누군가가 구조체에 #[serde(rename_all = "camelCase")]를 추가하여 응답 형식이 user_id에서 userId로 바뀌면, 이 변경 사항이 배포되기 전에 이 테스트는 실패합니다. 이것이 Apidog가 제공하지만 cargo test는 제공하지 않는 계약입니다. cargo test는 Rust 코드를 Rust 타입에 대해 실행하므로 어떤 형식이든 행복하게 통과할 것입니다.
4단계: Serde 거부 사례 다루기
Rust JSON 처리에서 흥미로운 부분은 Serde가 잘못된 입력에 대해 수행하는 작업입니다. 기본적으로 Axum은 세부 정보 없이 422 처리할 수 없는 엔티티 (Unprocessable Entity)를 반환합니다. 의도적으로 스키마를 깨는 세 가지 요청을 만듭니다:
| 요청 | 본문 | 예상 |
|---|---|---|
create-user-missing-email |
{ "name": "Ada" } |
422, 본문에 missing field email 언급 |
create-user-extra-field |
{ "name": "Ada", "email": "a@b.c", "admin": true } |
#[serde(deny_unknown_fields)]가 없으면 200; 그렇지 않으면 422 |
create-user-wrong-type |
{ "name": 1, "email": "a@b.c" } |
422, invalid type: integer 언급 |
테스트에서 각 상태 코드를 단언합니다. 이것은 실제 유효성 검사 정책을 문서화하는 가장 저렴한 방법입니다. 나중에 deny_unknown_fields를 켜면 두 번째 테스트가 빨간색으로 바뀌면서 공개 계약이 변경되었음을 알려줍니다.
5단계: JWT로 보호된 경로 테스트
대부분의 프로덕션 Rust API는 인증 미들웨어 뒤에 핸들러를 숨깁니다. Axum의 axum-extra JWT 추출기가 일반적인 패턴입니다:
use axum_extra::extract::cookie::PrivateCookieJar;
use jsonwebtoken::{decode, DecodingKey, Validation};
async fn me(jar: PrivateCookieJar) -> Result<Json<User>, StatusCode> {
let token = jar.get("token").ok_or(StatusCode::UNAUTHORIZED)?;
let claims = decode::<Claims>(token.value(), &DecodingKey::from_secret(b"secret"), &Validation::default())
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Json(User { id: claims.claims.sub, name: "Ada".into(), email: "ada@example.com".into() }))
}
Apidog에서는 매 테스트 실행마다 JWT를 수동으로 만들 필요가 없습니다. 폴더에 Pre-Request 스크립트를 생성합니다:
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{ sub: 1, exp: Math.floor(Date.now() / 1000) + 3600 },
"secret"
);
pm.environment.set("token", token);
폴더 설정을 열고, Auth를 Bearer Token으로, 값을 {{token}}으로 설정합니다. 이제 폴더의 모든 요청은 새로운 JWT를 서명하고 제시합니다. 테스트 실행에서 만료된 토큰 버그가 사라집니다. 단언 측면에 대한 더 자세한 내용은 API에서 JWT 인증을 테스트하는 방법을 참조하십시오.
6단계: 스트리밍 및 서버 전송 이벤트 테스트
Rust 웹 프레임워크는 일급 스트리밍을 지원합니다. Axum의 Sse 응답은 futures::Stream을 래핑하고 text/event-stream 청크를 내보냅니다. 와이어 형식은 프레임당 data: { ... }\n\n이며, 연결 종료 또는 명시적인 완료 이벤트로 종료됩니다.
이를 사용하는 요청은 다른 GET 요청과 동일하게 보이지만, Apidog의 응답 패널은 Content-Type이 text/event-stream일 때 스트리밍 모드로 전환됩니다. 각 프레임이 도착할 때마다 타임스탬프와 함께 볼 수 있습니다. 이는 역압 문제 또는 누락된 플러시를 디버깅할 때 필요한 보기입니다.
단언할 내용:
- 첫 번째 청크가 광고하는 SLA 내에 도착하는지 확인합니다. Apidog는 스트리밍 패널에 청크별 지연 시간을 표시합니다.
- 연결 종료 전에 특정 이벤트 이름이 발생하는지 확인합니다 (예:
event: done). - 전체 스트림 지속 시간이 제한되는지 확인합니다.
while let Some(event) = stream.next().await에서 벗어나는 것을 잊은 핸들러는 영원히 스트리밍될 것입니다. Apidog는 이를 보여줄 것이며, 요청 설정에서 테스트를 실패하게 할 하드 타임아웃을 설정할 수 있습니다.
엔드포인트가 SSE 대신 WebSocket을 사용하는 경우, Apidog는 별도의 WebSocket 요청 유형을 제공합니다. 패턴은 동일합니다: 한 번 연결을 구축하고, 메시지 시퀀스를 저장하고, 응답을 단언합니다.
7단계: 병렬 프런트엔드 개발을 위한 Rust API 모킹
프런트엔드는 Rust 컴파일 시간 때문에 거의 블록되지 않습니다. 아직 존재하지 않는 핸들러 때문에 블록됩니다. Apidog 모의는 핸들러가 배포되기 전에 프런트엔드와 합의한 계약을 반환하는 안정적인 URL을 게시할 수 있도록 합니다.
create-user를 마우스 오른쪽 버튼으로 클릭하고, Smart Mock을 선택한 다음 활성화합니다. 이제 Apidog는 https://mock.apidog.com/m1/<projectId>/users에서 합성된 User 응답을 제공합니다. 모의 본문은 저장된 예제와 일치합니다. 모의 URL은 동일한 본문 형식을 받아들이므로, 프런트엔드는 마치 실제 Rust 서버인 것처럼 POST 요청을 보낼 수 있습니다.
동적 모의의 경우, Advanced Mock으로 전환하여 스크립트를 작성합니다:
return {
id: Math.floor(Math.random() * 10000),
name: body.name,
email: body.email,
createdAt: new Date().toISOString()
};
이 모의는 프런트엔드가 보내는 내용에 대해 생성된 id와 타임스탬프와 함께 응답합니다. Rust 핸들러가 준비되면 프런트엔드는 기본 URL을 http://localhost:3000으로 다시 바꾸고 다른 것은 아무것도 변경되지 않습니다. 이 패턴에 대한 자세한 내용은 팀이 Spring Boot API 구축 및 테스트와 일반적인 API 테스트 워크플로우도 다루고 있습니다. 아이디어는 같지만 런타임이 다릅니다.
8단계: CI 테스트 시나리오로 저장
Apidog 테스트 시나리오는 공유 변수를 사용하여 요청을 연결하고 헤드리스로 실행합니다. 시나리오를 구축합니다:
health-check,200단언.create-user,200단언,body.id를 변수로 캡처.create-user-missing-email,422단언.me(JWT 사전 요청 포함),200단언 및 반환된id가 캡처된id와 일치하는지 단언.- SSE 요청, 스트림이 5초 이내에 완료되는지 단언.
시나리오를 JSON으로 내보내고, tests/apidog/ 아래에 있는 리포지토리에 커밋한 다음, CI에서 호출합니다:
- name: Run API contract tests
run: |
cargo build --release
./target/release/myserver &
sleep 2
apidog-cli run tests/apidog/contract.json --env "Rust Local"
이제 핸들러에 영향을 미치는 모든 PR은 전체 계약 스위트와 함께 실제 Rust 바이너리에 대해 실행됩니다. Serde 이름 변경, 상태 코드 변경 또는 JWT 유효성 검사 조정이 공개 형태를 깨뜨리면, 병합 버튼이 녹색으로 바뀌기 전에 CI에서 이를 감지합니다.
9단계: 저장된 요청에서 OpenAPI 생성
요청 세트가 안정적일 때, Apidog의 내보내기 메뉴를 열고 OpenAPI 3.1을 선택합니다. 모든 저장된 요청을 다루는 스펙 문서와 예제로 보낸 본문을 얻게 됩니다. 이를 타입이 지정된 클라이언트 (TypeScript, Swift, Kotlin, Python)를 생성하는 사람에게 전달하면, 6개월 전에 누군가가 .yaml에 수동으로 작성한 것이 아니라, 현재 Rust 서버가 반환하는 것과 일치하는 계약을 받게 됩니다.
스펙을 Rust 리포지토리에 체크인하려면, CI에서 apidog-cli export를 실행하여 openapi.json에 작성합니다. 다음 cargo build는 변경되지 않지만, API의 모든 소비자는 디스크에서 실제 정보를 얻게 됩니다.
자주 묻는 질문 (FAQ)
- Apidog는 Axum과 Actix-web 모두에서 작동하나요? 네. Apidog는 Rust가 아닌 HTTP와 통신합니다. 요청에 응답하는 모든 것(Axum, Actix-web, Rocket, Warp, Poem, Loco)은 동일하게 작동합니다. 유일한 Rust 관련 고려 사항은 로컬 테스트를 위해
127.0.0.1대신0.0.0.0에 바인딩하는 것입니다. - 패닉을 일으키는 핸들러는 어떻게 테스트하나요? 라우터 앞에
tower-http의CatchPanicLayer를 사용하여 서버를 실행합니다. 패닉은 JSON 본문과 함께500으로 변환됩니다. 패닉 경로를 트리거하는 Apidog 요청을 만들고500을 단언합니다. 패닉을 래핑하지 않으면 연결이 끊어지고 Apidog는 네트워크 오류를 보고하는데, 이것도 유효한 계약 테스트입니다. - Docker에서 Rust 바이너리에 대해 Apidog를 실행할 수 있나요? 네.
baseUrl을 컨테이너의 노출된 포트에 연결하면 됩니다. 컨테이너가 Docker Compose 내에서 실행되는 경우, Apidog 러너에 동일한 네트워크를 제공하거나 호스트의 매핑된 포트를 사용합니다. - gRPC는 어떻습니까? Apidog는 gRPC 요청 유형을 가지고 있습니다.
.proto파일을 가져오고, 서비스와 메서드를 선택하고, 요청 페이로드를 채우고, 전송합니다. 인증, 환경, 테스트 시나리오 패턴은 REST와 동일합니다. - 테스트 시나리오가
cargo test를 대체하나요? 아니요. Rust 코드에 대한 단위 테스트는 Rust에 유지됩니다. Apidog는 실행 중인 표면인 HTTP 계약을 테스트합니다. 두 계층은 다른 버그를 잡습니다. 단위 테스트는 깨진 함수를 잡고, Apidog 테스트는 깨진 응답 형식, 누락된 CORS 헤더 또는400이422로 바뀐 것을 잡습니다. 둘 다 필요합니다. - Apidog는 Rust 오픈소스 프로젝트에 무료인가요? 네. Apidog 클라이언트는 개인 및 소규모 팀에게 무료입니다. 테스트 시나리오, 모의, OpenAPI 내보내기는 무료 계층에 포함됩니다. 공개 Rust API를 유지 관리하는 경우, 프로젝트 파일을 리포지토리에 포함하여 클론하는 모든 사람이 테스트 스위트를 사용할 수 있도록 할 수 있습니다.
마무리
Rust API는 컴파일러를 기다리지 않는 피드백 루프를 가질 가치가 있습니다. Apidog의 요청 컬렉션은 이러한 루프를 제공합니다: 실제 HTTP, 실제 단언, 프런트엔드를 위한 실제 모의, 그리고 실시간 바이너리에 대해 실행되는 CI 시나리오. 위에 설명된 요청들을 한 번 구축하면, Axum 또는 Actix 핸들러에 대한 모든 미래 변경 사항은 런타임의 놀라움이 아닌 통제된 테스트 실행이 됩니다.
Apidog를 다운로드하여 Rust 서버에 연결해 보세요. 설정하는 데 10분도 채 걸리지 않습니다. 그 결과는 당신이 통제하고 cargo와 분리된 계약, 그리고 핸들러가 언제 완료되는지 묻는 것을 멈춘 프런트엔드 팀입니다.
