DEV Community

Cover image for 러스트 API 테스트 방법
Rihpig
Rihpig

Posted on • Originally published at apidog.com

러스트 API 테스트 방법

Rust를 사용하면 수백 줄 안에 빠르고 타입 안정적인 HTTP 서버를 만들 수 있습니다. 하지만 실행 중인 API를 빠르게 호출하고, 응답 계약을 검증하고, 프런트엔드와 모의를 공유하는 피드백 루프는 Rust 툴체인만으로는 부족합니다. cargo test는 중요하지만, 실제 HTTP 상태 코드, 헤더, JSON 형식, 인증 흐름을 확인하려면 실행 중인 서버와 통신하는 별도 도구가 필요합니다.

지금 Apidog를 사용해 보세요

이 글에서는 Apidog를 사용해 Rust API 테스트 워크플로우를 구성합니다. Axum 또는 Actix 서버를 Apidog에 연결하고, 요청을 저장하고, Serde 기반 JSON 응답을 검증하고, JWT 인증을 자동화하고, 미완성 핸들러를 모킹하며, 마지막으로 CI에서 계약 테스트를 실행하는 흐름까지 다룹니다.

Postman 또는 curl 중심 워크플로우를 사용하던 경우에도 동일한 방식으로 적용할 수 있습니다. Apidog는 저장된 요청에서 OpenAPI 스펙을 생성하고, 공유 가능한 모의 URL과 팀 환경을 제공합니다. Postman 전환 사례는 Postman 마이그레이션 이야기를 참고하고, 여기서는 Rust API에 집중합니다.

요약

  • Rust 서버를 로컬에서 실행합니다. 예: Axum 또는 Actix-web 프로젝트에서 cargo run
  • Apidog 환경에 http://localhost:3000baseUrl로 등록합니다.
  • GET /healthz 요청을 먼저 만들어 연결 상태를 확인합니다.
  • JSON 엔드포인트는 Serde 구조체와 실제 응답을 기준으로 테스트 스크립트를 작성합니다.
  • JWT는 Pre-Request Script에서 생성하고 {{token}} 변수로 저장합니다.
  • 폴더 수준에서 Bearer 인증을 설정해 모든 요청이 토큰을 상속하도록 합니다.
  • 미완성 Rust 핸들러는 Apidog Mock으로 대체해 프런트엔드 개발을 병렬로 진행합니다.
  • 테스트 시나리오를 저장한 뒤 apidog-cli로 CI에서 실행합니다.

Rust 툴체인 외부에서 API를 테스트해야 하는 이유

cargo test는 Rust 코드의 단위 테스트와 통합 테스트에 적합합니다. 하지만 실제 HTTP 계약을 검증하려면 다음 항목을 별도로 확인해야 합니다.

  • 올바른 상태 코드가 반환되는가?
  • JSON 필드명이 공개 계약과 일치하는가?
  • 오류 응답 형식이 유지되는가?
  • 인증 헤더 또는 쿠키가 올바르게 처리되는가?
  • 프런트엔드가 사용할 수 있는 모의 API가 있는가?

Rust 테스트 코드로도 가능하지만, 각 경로마다 tower::ServiceExt::oneshot 호출을 만들고 유지해야 합니다. 또한 프런트엔드 팀이 같은 계약을 호출하려면 별도 모의 서버를 다시 만들어야 합니다.

Apidog를 사용하면 실행 중인 서버 위에 별도의 계약 테스트 계층을 둘 수 있습니다.

  1. 빌드와 계약 검사를 분리할 수 있습니다.

    Apidog는 실행 중인 바이너리에 HTTP 요청을 보냅니다. 단순 응답 계약 확인을 위해 매번 rustc를 기다릴 필요가 없습니다.

  2. 모의를 공유할 수 있습니다.

    핸들러가 아직 완성되지 않아도 프런트엔드 팀은 합의된 JSON을 반환하는 URL을 사용할 수 있습니다.

  3. 저장된 요청에서 OpenAPI를 생성할 수 있습니다.

    모든 경로에 utoipa 또는 aide 어노테이션을 추가하지 않아도 저장된 요청을 기반으로 OpenAPI 3.1 문서를 만들 수 있습니다.

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();
}
Enter fullscreen mode Exit fullscreen mode

Apidog에서 새 프로젝트를 만든 뒤, 환경 관리 메뉴에서 Rust Local 환경을 추가합니다.

변수
baseUrl http://localhost:3000
token 비워두기
apiVersion v1

스테이징 서버가 있다면 Rust Staging 환경도 추가합니다.

예:

변수
baseUrl https://staging.example.com
token 비워두기
apiVersion v1

이렇게 설정하면 요청 URL을 직접 수정하지 않고 환경 드롭다운만 바꿔 로컬과 스테이징을 전환할 수 있습니다.

2단계: 첫 번째 엔드포인트 호출하기

Apidog 프로젝트 안에 Rust API 폴더를 만들고 새 요청을 추가합니다.

  • 메서드: GET
  • URL: {{baseUrl}}/healthz

전송하면 서버가 실행 중일 때 다음 응답을 받아야 합니다.

ok
Enter fullscreen mode Exit fullscreen mode

상태 코드는 200이어야 합니다. 이 요청을 health-check로 저장합니다.

이 요청은 가장 기본적인 스모크 테스트입니다. 이후 더 복잡한 JSON, 인증, 스트리밍 테스트를 작성하기 전에 다음을 확인합니다.

  • 서버가 실행 중인가?
  • 포트가 맞는가?
  • Apidog 환경 변수 baseUrl이 올바른가?
  • 로컬 서버가 외부 요청을 받을 수 있게 바인딩되어 있는가?

연결 거부 오류가 발생하면 바인딩 주소를 확인하세요. 로컬 개발에서는 다음과 같이 0.0.0.0에 바인딩하는 편이 안전합니다.

TcpListener::bind("0.0.0.0:3000").await.unwrap();
Enter fullscreen mode Exit fullscreen mode

127.0.0.1:3000에만 바인딩하면 Docker 컨테이너나 일부 네트워크 인터페이스에서 접근이 실패할 수 있습니다.

3단계: Serde 기반 JSON 요청과 응답 테스트하기

Rust API에서 가장 흔한 형태는 Serde 구조체를 사용하는 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));
Enter fullscreen mode Exit fullscreen mode

Apidog에서 새 요청을 만듭니다.

  • 메서드: POST
  • URL: {{baseUrl}}/users
  • Body 타입: JSON
{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

요청을 전송한 뒤 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(/^[^@]+@[^@]+$/);
});
Enter fullscreen mode Exit fullscreen mode

이 테스트는 공개 HTTP 계약을 검증합니다. 예를 들어 나중에 Serde 설정이 변경되어 응답 필드명이 바뀌면, Rust 타입 레벨에서는 문제가 없어도 Apidog 테스트는 실패합니다.

#[serde(rename_all = "camelCase")]
Enter fullscreen mode Exit fullscreen mode

이런 변경은 API 소비자에게 영향을 줄 수 있으므로 CI에서 먼저 잡는 것이 좋습니다.

4단계: Serde 거부 사례 테스트하기

성공 응답만 테스트하면 실제 계약의 절반만 확인한 것입니다. 잘못된 입력에 대한 응답도 저장해 두어야 합니다.

다음 요청을 추가합니다.

요청 이름 Body 예상 결과
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 관련 메시지

각 요청의 테스트 탭에서 상태 코드를 단언합니다.

예:

pm.test("Status is 422", () => {
  pm.expect(pm.response.code).to.eql(422);
});
Enter fullscreen mode Exit fullscreen mode

응답 본문까지 검증하려면 다음처럼 작성할 수 있습니다.

pm.test("Error mentions missing email", () => {
  pm.expect(pm.response.text()).to.include("email");
});
Enter fullscreen mode Exit fullscreen mode

이렇게 하면 유효성 검사 정책이 문서처럼 남습니다. 나중에 deny_unknown_fields를 켜면 create-user-extra-field 테스트가 실패하면서 공개 계약 변경을 알려줍니다.

5단계: JWT로 보호된 경로 테스트하기

대부분의 프로덕션 Rust API는 인증 미들웨어 뒤에 핸들러를 둡니다. Axum에서 쿠키 기반 JWT를 읽는 예시는 다음과 같습니다.

use axum::{
    http::StatusCode,
    Json,
};
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(),
    }))
}
Enter fullscreen mode Exit fullscreen mode

Apidog에서는 매번 JWT를 수동으로 만들 필요가 없습니다. 폴더 수준에 Pre-Request Script를 추가합니다.

const jwt = require("jsonwebtoken");

const token = jwt.sign(
  {
    sub: 1,
    exp: Math.floor(Date.now() / 1000) + 3600
  },
  "secret"
);

pm.environment.set("token", token);
Enter fullscreen mode Exit fullscreen mode

그 다음 폴더 설정에서 인증을 설정합니다.

  • Auth 타입: Bearer Token
  • Token 값: {{token}}

이제 해당 폴더 안의 모든 요청은 실행 시점에 새 JWT를 만들고 자동으로 전송합니다.

JWT 인증 테스트의 추가 패턴은 API에서 JWT 인증을 테스트하는 방법을 참고할 수 있습니다.

6단계: 스트리밍 및 Server-Sent Events 테스트하기

Rust 웹 프레임워크는 스트리밍 응답을 잘 지원합니다. Axum의 Sse 응답은 futures::Stream을 감싸고 text/event-stream 청크를 반환합니다.

SSE 와이어 형식은 보통 다음과 같습니다.

data: { ... }

Enter fullscreen mode Exit fullscreen mode

Apidog에서 SSE 엔드포인트는 일반 GET 요청처럼 만들 수 있습니다.

  • 메서드: GET
  • URL: {{baseUrl}}/events

응답 Content-Typetext/event-stream이면 Apidog 응답 패널에서 스트림을 확인할 수 있습니다.

테스트할 항목은 다음과 같습니다.

  • 첫 번째 청크가 예상 시간 안에 도착하는가?
  • 특정 이벤트가 종료 전에 발생하는가? 예: event: done
  • 스트림이 무한히 열려 있지 않고 제한 시간 안에 끝나는가?
  • 요청 설정의 타임아웃이 적절히 설정되어 있는가?

Rust 핸들러에서 다음과 같은 루프가 종료 조건 없이 작성되면 스트림이 계속 열려 있을 수 있습니다.

while let Some(event) = stream.next().await {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Apidog에서 스트리밍 응답을 직접 보면 누락된 flush, 지연, 종료되지 않는 스트림을 더 쉽게 확인할 수 있습니다.

WebSocket을 사용하는 경우에는 Apidog의 WebSocket 요청 유형을 사용합니다. 연결을 만들고, 메시지 시퀀스를 저장하고, 응답 메시지를 단언하는 방식은 SSE와 비슷합니다.

7단계: 프런트엔드 개발을 위한 Rust API 모킹하기

프런트엔드 개발은 Rust 컴파일 시간보다 “아직 구현되지 않은 핸들러” 때문에 더 자주 막힙니다. Apidog Mock을 사용하면 실제 Rust 핸들러가 준비되기 전에 합의된 응답 계약을 URL로 공유할 수 있습니다.

예를 들어 저장된 create-user 요청에서 다음을 수행합니다.

  1. create-user 요청을 우클릭합니다.
  2. Smart Mock을 선택합니다.
  3. Mock을 활성화합니다.

그러면 Apidog는 다음과 유사한 모의 URL을 제공합니다.

https://mock.apidog.com/m1/<projectId>/users
Enter fullscreen mode Exit fullscreen mode

이 URL은 저장된 예제 응답과 일치하는 User JSON을 반환합니다. 프런트엔드는 실제 Rust 서버 대신 이 URL로 POST 요청을 보내 UI 작업을 계속할 수 있습니다.

동적인 응답이 필요하면 Advanced Mock을 사용합니다.

return {
  id: Math.floor(Math.random() * 10000),
  name: body.name,
  email: body.email,
  createdAt: new Date().toISOString()
};
Enter fullscreen mode Exit fullscreen mode

이 스크립트는 요청 본문에서 name, email을 읽고 동적으로 id, createdAt을 생성합니다.

Rust 핸들러가 완성되면 프런트엔드는 기본 URL만 다시 바꾸면 됩니다.

https://mock.apidog.com/m1/<projectId>
Enter fullscreen mode Exit fullscreen mode

에서

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

로 변경하고, 요청 형식은 그대로 유지합니다.

비슷한 워크플로우는 Spring Boot API 구축 및 테스트일반적인 API 테스트 워크플로우에서도 확인할 수 있습니다. 런타임은 다르지만 계약 기반 개발 패턴은 같습니다.

8단계: CI 테스트 시나리오로 저장하기

Apidog 테스트 시나리오는 여러 요청을 순서대로 실행하고, 변수와 단언을 공유할 수 있게 해줍니다.

예를 들어 다음 시나리오를 만듭니다.

  1. health-check

    • 200 단언
  2. create-user

    • 200 단언
    • body.id를 변수로 저장
  3. create-user-missing-email

    • 422 단언
  4. me

    • JWT Pre-Request Script 사용
    • 200 단언
    • 반환된 id가 앞에서 저장한 id와 일치하는지 확인
  5. SSE 요청

    • 스트림이 5초 이내에 완료되는지 확인

create-user 응답의 id를 변수로 저장하려면 테스트 스크립트에서 다음처럼 작성할 수 있습니다.

const body = pm.response.json();

pm.environment.set("createdUserId", body.id);
Enter fullscreen mode Exit fullscreen mode

me 요청에서는 다음처럼 비교합니다.

pm.test("Returned id matches created user id", () => {
  const body = pm.response.json();

  pm.expect(String(body.id)).to.eql(pm.environment.get("createdUserId"));
});
Enter fullscreen mode Exit fullscreen mode

시나리오를 JSON으로 내보낸 뒤 리포지토리에 커밋합니다.

tests/apidog/contract.json
Enter fullscreen mode Exit fullscreen mode

CI에서는 실제 Rust 바이너리를 실행한 뒤 apidog-cli로 계약 테스트를 실행합니다.

- name: Run API contract tests
  run: |
    cargo build --release
    ./target/release/myserver &
    sleep 2
    apidog-cli run tests/apidog/contract.json --env "Rust Local"
Enter fullscreen mode Exit fullscreen mode

이제 핸들러에 영향을 주는 PR은 병합 전에 실제 HTTP 계약 검사를 통과해야 합니다.

잡을 수 있는 문제는 다음과 같습니다.

  • Serde 필드명 변경
  • 상태 코드 변경
  • JWT 검증 방식 변경
  • 오류 응답 형식 변경
  • 스트리밍 응답 종료 누락
  • 인증 헤더 누락

9단계: 저장된 요청에서 OpenAPI 생성하기

요청 세트가 안정되면 Apidog의 내보내기 메뉴에서 OpenAPI 3.1을 선택합니다.

생성되는 결과에는 다음이 포함됩니다.

  • 저장된 경로
  • 요청 메서드
  • 요청 본문 예제
  • 응답 예제
  • API 계약 문서

이 스펙은 TypeScript, Swift, Kotlin, Python 등의 타입 클라이언트를 생성하는 팀에게 전달할 수 있습니다.

Rust 리포지토리에 스펙을 포함하려면 CI에서 다음과 같은 흐름으로 관리할 수 있습니다.

apidog-cli export --output openapi.json
Enter fullscreen mode Exit fullscreen mode

이렇게 하면 API 소비자는 수동으로 작성된 오래된 YAML이 아니라 현재 요청 컬렉션에서 나온 계약을 기준으로 작업할 수 있습니다.

자주 묻는 질문

Apidog는 Axum과 Actix-web 모두에서 작동하나요?

네. Apidog는 Rust 프레임워크가 아니라 HTTP와 통신합니다. Axum, Actix-web, Rocket, Warp, Poem, Loco처럼 요청에 응답하는 서버라면 같은 방식으로 테스트할 수 있습니다.

Rust 쪽에서 주의할 점은 로컬 테스트 시 127.0.0.1 대신 0.0.0.0에 바인딩하는 것입니다.

패닉이 발생하는 핸들러는 어떻게 테스트하나요?

tower-httpCatchPanicLayer를 라우터 앞에 추가하면 패닉을 500 응답으로 변환할 수 있습니다. 그 후 Apidog에서 해당 경로를 호출하고 500을 단언합니다.

패닉을 래핑하지 않으면 연결이 끊어지고 Apidog는 네트워크 오류를 보고합니다. 이것도 API의 실제 동작이므로 계약 테스트 관점에서는 기록할 수 있습니다.

Docker에서 실행 중인 Rust 바이너리도 테스트할 수 있나요?

네. baseUrl을 컨테이너가 노출한 포트로 설정하면 됩니다.

Docker Compose 내부에서 실행 중이라면 다음 중 하나를 선택합니다.

  • Apidog 러너를 같은 네트워크에서 실행
  • 호스트에 매핑된 포트를 사용

예:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

gRPC도 테스트할 수 있나요?

Apidog는 gRPC 요청 유형도 제공합니다.

일반적인 흐름은 다음과 같습니다.

  1. .proto 파일을 가져옵니다.
  2. 서비스와 메서드를 선택합니다.
  3. 요청 페이로드를 작성합니다.
  4. 인증과 환경 변수를 설정합니다.
  5. 테스트 시나리오에 포함합니다.

환경, 인증, 테스트 시나리오 패턴은 REST API와 동일합니다.

Apidog 테스트 시나리오가 cargo test를 대체하나요?

아니요.

cargo test는 Rust 코드의 단위 테스트와 내부 로직 검증에 필요합니다. Apidog는 실행 중인 HTTP 표면을 테스트합니다.

둘은 서로 다른 버그를 잡습니다.

  • cargo test: 함수 로직, 타입, 모듈 단위 동작
  • Apidog: 응답 형식, 상태 코드, 헤더, 인증, CORS, 공개 API 계약

둘 다 유지하는 것이 좋습니다.

Apidog는 Rust 오픈소스 프로젝트에 무료인가요?

네. Apidog 클라이언트는 개인 및 소규모 팀에게 무료입니다. 테스트 시나리오, 모의, OpenAPI 내보내기는 무료 계층에 포함됩니다.

공개 Rust API를 유지 관리한다면 Apidog 프로젝트 파일 또는 내보낸 테스트 시나리오를 리포지토리에 포함해, 클론한 개발자가 같은 테스트 스위트를 실행할 수 있게 만들 수 있습니다.

마무리

Rust API에는 컴파일러와 별개로 빠르게 실행되는 HTTP 계약 테스트 루프가 필요합니다. Apidog를 사용하면 요청 컬렉션, 테스트 스크립트, JWT 자동화, 모의 API, CI 시나리오를 한곳에서 관리할 수 있습니다.

추천하는 최소 구성은 다음과 같습니다.

  1. health-check 요청 추가
  2. 주요 JSON 엔드포인트 요청 저장
  3. 성공 및 실패 케이스 테스트 작성
  4. JWT 생성 자동화
  5. 미완성 엔드포인트 Mock 활성화
  6. 테스트 시나리오를 CI에 연결
  7. 필요 시 OpenAPI 3.1로 내보내기

Apidog를 다운로드하고 로컬 Rust 서버에 연결해 보세요. 설정은 짧지만, 그 결과로 빌드와 분리된 API 계약, 공유 가능한 모의 URL, CI에서 실행되는 실제 HTTP 테스트를 얻을 수 있습니다.

Top comments (0)