이미지 로딩 중...
AI Generated
2025. 11. 13. · 4 Views
JSON 요청 본문과 헤더를 포함한 테스트 작성 완벽 가이드
API 테스트에서 JSON 요청 본문과 인증 헤더를 포함한 복잡한 요청을 테스트하는 방법을 배웁니다. 실무에서 자주 사용되는 Jest와 Supertest를 활용한 통합 테스트 작성법을 단계별로 안내합니다.
목차
- 기본 JSON 요청 테스트
- Authorization 헤더 추가
- 여러 헤더 조합 테스트
- 요청 본문 검증 테스트
- 중첩된 JSON 객체 테스트
- 파일 업로드와 JSON 결합
- 쿼리 파라미터와 본문 결합
- 응답 상태 코드 검증
1. 기본 JSON 요청 테스트
시작하며
여러분이 회원 가입 API를 개발했는데, 프론트엔드에서 "계속 400 에러가 나요"라는 연락을 받은 적 있나요? 문제를 찾으려고 Postman으로 테스트하면 잘 되는데, 실제 요청에서는 자꾸 실패하는 상황이죠.
이런 문제는 요청 본문의 형식, Content-Type 헤더, 또는 JSON 직렬화 문제 때문에 발생합니다. 수동으로 매번 테스트하기보다는 자동화된 테스트를 작성하면 이런 문제를 사전에 방지할 수 있습니다.
바로 이럴 때 필요한 것이 JSON 요청 본문 테스트입니다. API가 올바른 JSON 형식을 받아들이고 예상대로 응답하는지 자동으로 검증할 수 있습니다.
개요
간단히 말해서, JSON 요청 본문 테스트는 API 엔드포인트에 JSON 데이터를 전송하고 그 결과를 검증하는 자동화된 테스트입니다. 실무에서는 대부분의 RESTful API가 JSON 형식으로 데이터를 주고받습니다.
회원 가입, 로그인, 게시글 작성 등 거의 모든 POST/PUT/PATCH 요청에서 JSON을 사용하죠. 이런 요청들이 제대로 작동하는지 확인하려면 테스트가 필수입니다.
기존에는 Postman이나 cURL로 수동 테스트를 했다면, 이제는 Jest와 Supertest를 사용해 자동화할 수 있습니다. 코드가 변경될 때마다 자동으로 실행되어 문제를 즉시 발견합니다.
핵심 특징은 첫째, 실제 HTTP 요청을 시뮬레이션한다는 점입니다. 둘째, JSON 직렬화와 Content-Type 헤더를 자동으로 처리합니다.
셋째, 응답 본문과 상태 코드를 동시에 검증할 수 있습니다. 이러한 특징들이 API의 안정성을 크게 향상시킵니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('POST /api/users', () => {
it('should create a new user with JSON body', async () => {
// JSON 요청 본문 준비
const newUser = {
name: 'John Doe',
email: 'john@example.com',
password: 'securePass123'
};
// POST 요청 전송 및 검증
const response = await request(app)
.post('/api/users')
.send(newUser) // JSON 자동 직렬화
.expect('Content-Type', /json/)
.expect(201);
// 응답 본문 검증
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('John Doe');
});
});
설명
이것이 하는 일: 이 테스트는 사용자 생성 API에 JSON 형식의 요청을 보내고, 서버가 올바르게 처리하여 201 상태 코드와 함께 생성된 사용자 정보를 반환하는지 검증합니다. 첫 번째로, newUser 객체를 JavaScript 일반 객체로 정의합니다.
이렇게 하는 이유는 Supertest가 자동으로 JSON.stringify()를 호출해 주기 때문입니다. 수동으로 문자열로 변환할 필요가 없어 코드가 깔끔해집니다.
그 다음으로, request(app).post().send(newUser)가 실행되면서 내부적으로 여러 일이 발생합니다. Supertest는 객체를 JSON 문자열로 변환하고, Content-Type 헤더를 'application/json'으로 자동 설정하며, 실제 HTTP POST 요청을 생성합니다.
이 모든 과정이 한 줄의 .send() 호출로 처리됩니다. 마지막으로, .expect() 체이닝을 통해 응답을 검증합니다.
.expect('Content-Type', /json/)은 응답이 JSON 형식인지 확인하고, .expect(201)은 성공적인 생성을 나타내는 상태 코드를 검증합니다. 그리고 response.body를 통해 실제 응답 데이터의 구조와 값을 확인합니다.
여러분이 이 코드를 사용하면 API가 JSON 요청을 올바르게 파싱하고 처리하는지 자동으로 확인할 수 있습니다. 코드 변경 시 즉시 테스트가 실행되어 회귀 버그를 방지하고, 문서화 효과도 얻을 수 있으며, 프론트엔드 개발자와의 커뮤니케이션 비용도 줄어듭니다.
실전 팁
💡 .send()를 사용하면 Content-Type이 자동으로 설정되지만, .set('Content-Type', 'application/json')으로 명시적으로 지정하면 더 명확합니다
💡 JSON 파싱 에러가 발생하면 서버 로그를 확인하세요. body-parser나 express.json() 미들웨어가 제대로 설정되었는지 점검이 필요합니다
💡 테스트 데이터는 별도의 fixtures 파일로 분리하면 재사용성이 높아지고 테스트 코드가 간결해집니다
💡 대용량 JSON 요청을 테스트할 때는 서버의 body size limit 설정을 확인하세요. Express의 기본값은 100kb입니다
💡 응답 본문 검증 시 toMatchObject()를 사용하면 전체 구조를 검증하지 않고 필요한 필드만 체크할 수 있습니다
2. Authorization 헤더 추가
시작하며
여러분이 로그인이 필요한 마이페이지 API를 만들었는데, "로그인했는데도 401 에러가 나요"라는 버그 리포트를 받은 적 있나요? 토큰은 제대로 발급되었는데, 인증 헤더가 올바른 형식으로 전달되지 않아서 발생하는 문제죠.
이런 문제는 Authorization 헤더의 형식(Bearer, Basic 등), 토큰의 인코딩, 또는 대소문자 차이 때문에 발생합니다. 인증이 필요한 엔드포인트는 특히 테스트가 중요한데, 수동으로 매번 토큰을 복사해서 테스트하기는 번거롭습니다.
바로 이럴 때 필요한 것이 Authorization 헤더를 포함한 테스트입니다. 인증 토큰을 자동으로 포함시켜 보호된 API가 올바르게 동작하는지 검증할 수 있습니다.
개요
간단히 말해서, Authorization 헤더 테스트는 JWT 토큰이나 API 키 같은 인증 정보를 요청에 포함시켜 보호된 엔드포인트의 접근 제어가 제대로 작동하는지 확인하는 테스트입니다. 실무에서는 대부분의 API가 인증을 요구합니다.
사용자 프로필 조회, 게시글 수정, 결제 처리 등 민감한 작업에서는 반드시 인증이 필요하죠. 이런 엔드포인트들이 올바른 토큰은 허용하고, 잘못된 토큰은 거부하는지 테스트해야 합니다.
기존에는 Postman에서 매번 토큰을 복사-붙여넣기 했다면, 이제는 테스트 코드에서 자동으로 토큰을 생성하거나 fixture에서 가져와 사용할 수 있습니다. 토큰 갱신 로직도 테스트에 포함시킬 수 있죠.
핵심 특징은 첫째, .set() 메서드로 모든 HTTP 헤더를 자유롭게 추가할 수 있다는 점입니다. 둘째, Bearer 토큰, Basic 인증, API 키 등 다양한 인증 방식을 테스트할 수 있습니다.
셋째, 인증 실패 시나리오(만료된 토큰, 잘못된 형식)도 함께 테스트할 수 있습니다. 이러한 특징들이 보안 취약점을 사전에 발견하게 해줍니다.
코드 예제
const request = require('supertest');
const app = require('../app');
const jwt = require('jsonwebtoken');
describe('GET /api/profile', () => {
let authToken;
beforeAll(() => {
// 테스트용 JWT 토큰 생성
authToken = jwt.sign(
{ userId: 123, role: 'user' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
});
it('should get user profile with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`) // 인증 헤더 추가
.expect(200);
expect(response.body.userId).toBe(123);
expect(response.body.email).toBeDefined();
});
it('should reject request without token', async () => {
await request(app)
.get('/api/profile')
.expect(401); // 인증 실패
});
});
설명
이것이 하는 일: 이 테스트는 보호된 프로필 조회 API에 유효한 JWT 토큰을 포함한 요청을 보내고, 서버가 토큰을 검증하여 올바른 사용자 정보를 반환하는지 확인합니다. 또한 토큰이 없는 경우 접근이 거부되는지도 검증합니다.
첫 번째로, beforeAll() 훅에서 테스트용 JWT 토큰을 생성합니다. 이렇게 하는 이유는 실제 로그인 과정을 거치지 않고도 유효한 토큰을 얻을 수 있기 때문입니다.
프로덕션과 동일한 시크릿 키를 사용하여 서버가 실제로 검증할 수 있는 토큰을 만듭니다. 그 다음으로, .set('Authorization', Bearer ${authToken})가 실행되면서 HTTP 요청 헤더에 인증 정보가 추가됩니다.
'Bearer'는 OAuth 2.0 표준에서 사용하는 토큰 타입입니다. 서버의 인증 미들웨어는 이 헤더를 읽어 토큰을 추출하고, 서명을 검증하며, 페이로드에서 사용자 정보를 꺼냅니다.
마지막으로, 두 가지 시나리오를 테스트합니다. 첫 번째 테스트는 유효한 토큰으로 성공하는 경우를, 두 번째 테스트는 토큰 없이 요청하여 401 상태 코드를 받는 경우를 검증합니다.
이렇게 positive와 negative 케이스를 모두 테스트하면 인증 로직의 완전성을 확인할 수 있습니다. 여러분이 이 코드를 사용하면 인증이 필요한 모든 API를 자동으로 테스트할 수 있습니다.
토큰 만료, 권한 부족, 잘못된 형식 등 다양한 인증 실패 시나리오를 커버하여 보안 버그를 사전에 방지할 수 있고, 인증 미들웨어의 변경 사항이 기존 기능을 깨뜨리지 않는지 즉시 확인할 수 있습니다.
실전 팁
💡 실제 프로덕션 시크릿 키 대신 테스트 환경용 별도 키를 사용하세요. .env.test 파일을 만들어 관리하면 안전합니다
💡 만료된 토큰 테스트를 위해 expiresIn을 '0s'로 설정하거나, 과거 타임스탬프로 토큰을 생성할 수 있습니다
💡 여러 테스트에서 인증이 필요하다면 헬퍼 함수를 만드세요. 예: getAuthHeader() 함수로 중복을 제거할 수 있습니다
💡 역할 기반 인증(RBAC)을 테스트할 때는 다양한 role 값으로 토큰을 생성하여 권한 체크를 검증하세요
💡 refresh token 로직도 함께 테스트하세요. 액세스 토큰 만료 후 리프레시 토큰으로 갱신하는 플로우를 자동화할 수 있습니다
3. 여러 헤더 조합 테스트
시작하며
여러분이 다국어를 지원하는 API를 개발했는데, "한국어 설정했는데 영어로 나와요"라거나 "특정 버전에서만 에러가 나요"라는 피드백을 받은 적 있나요? 요청 헤더의 조합에 따라 다르게 동작해야 하는데, 모든 경우의 수를 수동으로 테스트하기는 불가능에 가깝죠.
이런 문제는 Accept-Language, API-Version, Custom-Client-Id 같은 다양한 헤더들의 조합 때문에 발생합니다. 각 헤더는 독립적으로는 잘 작동하지만, 함께 사용될 때 예상치 못한 동작이 나타날 수 있습니다.
바로 이럴 때 필요한 것이 여러 헤더 조합 테스트입니다. Content-Type, Authorization, 커스텀 헤더 등을 동시에 설정하여 실제 프로덕션 환경의 요청을 정확히 재현할 수 있습니다.
개요
간단히 말해서, 여러 헤더 조합 테스트는 하나의 요청에 여러 개의 HTTP 헤더를 동시에 설정하여 서버가 복잡한 요청을 올바르게 처리하는지 검증하는 테스트입니다. 실무에서는 API 요청에 평균 3-5개 이상의 헤더가 포함됩니다.
인증 토큰, 콘텐츠 타입, 언어 설정, API 버전, 클라이언트 식별자, 타임존, 디바이스 정보 등이 모두 헤더로 전달되죠. 이런 복잡한 요청들이 올바르게 처리되는지 확인해야 합니다.
기존에는 Postman의 헤더 탭에서 하나씩 추가하며 테스트했다면, 이제는 .set()을 여러 번 체이닝하거나 객체로 한 번에 전달할 수 있습니다. 헤더 조합별로 테스트 케이스를 작성하여 모든 시나리오를 커버할 수 있죠.
핵심 특징은 첫째, .set() 메서드를 체이닝하여 무제한으로 헤더를 추가할 수 있다는 점입니다. 둘째, 객체 형태로 한 번에 여러 헤더를 전달할 수도 있습니다.
셋째, 각 헤더가 서버에서 올바르게 파싱되고 비즈니스 로직에 반영되는지 검증할 수 있습니다. 이러한 특징들이 복잡한 API의 품질을 보장합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('POST /api/posts with multiple headers', () => {
it('should handle multiple headers correctly', async () => {
const postData = {
title: 'New Post',
content: 'Post content'
};
const response = await request(app)
.post('/api/posts')
.set('Authorization', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')
.set('Accept-Language', 'ko-KR') // 다국어 지원
.set('API-Version', 'v2') // API 버전
.set('X-Client-Id', 'mobile-app-v1.2.3') // 클라이언트 식별
.set('X-Request-Id', 'req-12345') // 요청 추적
.send(postData)
.expect(201);
// 언어별 응답 확인
expect(response.body.message).toBe('게시글이 생성되었습니다');
// 헤더가 로그에 기록되었는지 확인
expect(response.body.requestId).toBe('req-12345');
});
});
설명
이것이 하는 일: 이 테스트는 게시글 생성 API에 인증, 언어, 버전, 클라이언트 정보 등 여러 헤더를 동시에 포함한 요청을 보내고, 서버가 각 헤더를 올바르게 인식하여 적절한 언어로 응답하고 요청을 추적할 수 있는지 검증합니다. 첫 번째로, 5개의 서로 다른 .set() 호출을 체이닝합니다.
이렇게 하는 이유는 각 헤더가 서로 다른 목적을 가지고 있어 명시적으로 구분하는 것이 가독성에 좋기 때문입니다. Authorization은 인증을, Accept-Language는 응답 언어를, API-Version은 엔드포인트 동작 버전을 제어합니다.
그 다음으로, 이 헤더들이 서버에 전달되면 여러 미들웨어가 순차적으로 처리합니다. 인증 미들웨어는 Authorization 헤더를 검증하고, i18n 미들웨어는 Accept-Language를 읽어 응답 메시지의 언어를 결정하며, 버전 미들웨어는 API-Version에 따라 라우팅을 조정합니다.
X-Request-Id는 로깅 시스템에서 요청을 추적하는 데 사용됩니다. 마지막으로, 응답을 검증할 때 각 헤더의 효과를 확인합니다.
response.body.message가 한국어인지 체크하여 Accept-Language가 작동했는지 확인하고, requestId가 응답에 포함되었는지 확인하여 요청 추적이 가능한지 검증합니다. 이렇게 헤더별로 기대 결과를 체크하면 각 헤더가 독립적으로 그리고 조합되어 올바르게 작동하는지 알 수 있습니다.
여러분이 이 코드를 사용하면 프로덕션 환경의 복잡한 요청을 테스트 환경에서 완벽히 재현할 수 있습니다. 다국어 지원이 올바른지, API 버저닝이 잘 작동하는지, 클라이언트별 특별 처리가 되는지 자동으로 검증하여 사용자 경험을 개선하고, 요청 추적 시스템이 제대로 동작하는지 확인하여 디버깅을 쉽게 만들 수 있습니다.
실전 팁
💡 헤더가 많아지면 객체로 한 번에 전달하세요: .set({ 'Authorization': '...', 'Accept-Language': 'ko-KR' }) 형식도 가능합니다
💡 커스텀 헤더는 X- 접두사를 사용하는 것이 관례입니다. X-Request-Id, X-Client-Version 등으로 명명하세요
💡 Accept-Language 테스트 시 'ko-KR', 'en-US', 'ja-JP' 등 다양한 로케일을 테스트하여 i18n이 제대로 작동하는지 확인하세요
💡 API 버전별로 별도의 describe 블록을 만들면 버전 간 차이를 명확히 문서화할 수 있습니다
💡 헤더 값에 특수문자가 포함될 때(예: Bearer 토큰)는 encodeURIComponent()가 필요한지 확인하세요. 대부분은 자동 처리되지만 커스텀 헤더는 주의가 필요합니다
4. 요청 본문 검증 테스트
시작하며
여러분이 회원 가입 폼을 만들었는데, 사용자가 이메일 형식이 틀린 채로 제출하거나 필수 항목을 비워서 서버가 500 에러를 내는 경우를 겪어본 적 있나요? 클라이언트에서 검증을 하더라도 악의적인 요청이나 버그로 인해 잘못된 데이터가 서버에 도달할 수 있습니다.
이런 문제는 서버 측 유효성 검증(validation)이 제대로 구현되지 않았거나, 에러 응답 형식이 일관되지 않아서 발생합니다. 잘못된 데이터가 데이터베이스에 저장되면 데이터 무결성이 깨지고, 나중에 더 큰 문제를 일으킵니다.
바로 이럴 때 필요한 것이 요청 본문 검증 테스트입니다. 다양한 잘못된 입력 케이스를 테스트하여 서버가 적절한 에러 메시지와 상태 코드로 응답하는지 확인할 수 있습니다.
개요
간단히 말해서, 요청 본문 검증 테스트는 의도적으로 잘못된 데이터를 전송하여 서버의 유효성 검사 로직이 올바르게 작동하고 명확한 에러 메시지를 반환하는지 확인하는 테스트입니다. 실무에서는 사용자 입력의 약 20-30%가 첫 시도에서 검증 에러를 일으킵니다.
이메일 오타, 비밀번호 규칙 미충족, 필수 필드 누락 등이 흔하죠. 이런 경우 서버가 500 에러 대신 400 Bad Request와 함께 "이메일 형식이 올바르지 않습니다" 같은 친절한 메시지를 보내야 합니다.
기존에는 일일이 틀린 데이터를 입력해가며 수동으로 테스트했다면, 이제는 자동화된 테스트로 수십 가지 검증 케이스를 몇 초 만에 확인할 수 있습니다. 각 필드별 검증 규칙이 모두 작동하는지 빠뜨리지 않고 체크할 수 있죠.
핵심 특징은 첫째, negative testing(실패 케이스 테스트)에 집중한다는 점입니다. 둘째, 각 검증 규칙별로 독립적인 테스트를 작성하여 어떤 규칙이 깨졌는지 명확히 알 수 있습니다.
셋째, 에러 응답의 구조와 메시지를 검증하여 프론트엔드가 사용자에게 적절한 피드백을 줄 수 있게 합니다. 이러한 특징들이 사용자 경험을 크게 개선합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('POST /api/users - Validation', () => {
// 필수 필드 누락
it('should return 400 when email is missing', async () => {
const invalidUser = {
name: 'John Doe',
password: 'pass123'
// email 누락
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
// 에러 메시지 구조 확인
expect(response.body).toHaveProperty('errors');
expect(response.body.errors[0].field).toBe('email');
expect(response.body.errors[0].message).toContain('필수');
});
// 형식 검증
it('should return 400 when email format is invalid', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'not-an-email', // 잘못된 형식
password: 'pass123'
})
.expect(400);
expect(response.body.errors[0].message).toContain('이메일 형식');
});
// 길이 검증
it('should return 400 when password is too short', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'john@example.com',
password: '123' // 너무 짧음
})
.expect(400);
expect(response.body.errors[0].field).toBe('password');
});
});
설명
이것이 하는 일: 이 테스트들은 회원 가입 API에 다양한 형태의 잘못된 데이터를 전송하여 서버의 유효성 검사 로직이 각 케이스를 올바르게 감지하고, 400 상태 코드와 함께 어떤 필드가 왜 잘못되었는지 알려주는 구조화된 에러 메시지를 반환하는지 검증합니다. 첫 번째로, 세 가지 다른 검증 실패 시나리오를 각각 독립적인 테스트로 작성합니다.
이렇게 하는 이유는 하나의 테스트에서 여러 검증을 체크하면 첫 번째 검증이 실패할 때 나머지를 확인할 수 없기 때문입니다. 필수 필드 누락, 형식 오류, 길이 제약을 따로 테스트하면 각 규칙이 독립적으로 작동하는지 명확히 알 수 있습니다.
그 다음으로, 각 테스트에서 .expect(400)을 사용하여 클라이언트 에러 상태 코드를 확인합니다. 500 에러가 아니라 400을 반환하는지 확인하는 것이 중요한데, 이는 서버가 예외를 던지지 않고 검증 실패를 우아하게 처리했다는 의미입니다.
그리고 response.body.errors 배열을 검증하여 에러 응답이 일관된 구조를 가지는지 확인합니다. 마지막으로, 각 에러 메시지의 내용을 확인합니다.
field 속성으로 어떤 필드가 문제인지, message 속성으로 무엇이 잘못되었는지 알 수 있어야 합니다. 이런 구조화된 에러 응답은 프론트엔드에서 각 입력 필드 옆에 정확한 에러 메시지를 표시할 수 있게 해줍니다.
'필수', '이메일 형식' 같은 키워드를 포함하는지 체크하여 메시지가 사용자에게 도움이 되는 내용인지 확인합니다. 여러분이 이 코드를 사용하면 데이터베이스에 잘못된 데이터가 저장되는 것을 방지할 수 있습니다.
모든 검증 규칙이 빠짐없이 작동하는지 자동으로 확인하고, 일관된 에러 응답 형식을 유지하여 프론트엔드 개발을 쉽게 만들며, 보안 취약점(SQL 인젝션, XSS 등)을 사전에 차단할 수 있습니다.
실전 팁
💡 검증 라이브러리(Joi, express-validator, Yup)를 사용하면 테스트 작성이 쉬워집니다. 스키마를 정의하면 자동으로 검증되고 에러 메시지가 생성됩니다
💡 에러 응답 형식을 표준화하세요. { errors: [{ field, message, code }] } 같은 일관된 구조를 사용하면 프론트엔드에서 파싱하기 쉽습니다
💡 경계값 테스트(boundary testing)를 추가하세요. 최소/최대 길이의 경계에서 정확히 검증되는지 확인합니다(예: 비밀번호 7자 vs 8자)
💡 다국어 에러 메시지도 테스트하세요. Accept-Language 헤더에 따라 영어/한국어 메시지가 올바르게 반환되는지 확인합니다
💡 SQL 인젝션, XSS 공격 문자열을 테스트 데이터로 사용하여 입력 sanitization이 작동하는지 검증하세요
5. 중첩된 JSON 객체 테스트
시작하며
여러분이 주문 시스템을 개발하는데, 주문 정보에 배송지 주소, 여러 개의 상품, 각 상품의 옵션 등이 중첩된 구조로 들어있다면 어떨까요? 단순한 평면 객체가 아니라 깊이 3-4단계의 복잡한 JSON 구조를 테스트해야 하는 상황이죠.
이런 문제는 현대 API에서 매우 흔합니다. 주문, 설문조사, 설정 정보 등은 모두 중첩된 객체와 배열을 포함합니다.
각 레벨의 데이터가 올바르게 파싱되고 저장되는지, 깊은 레벨의 필드 검증도 작동하는지 확인해야 합니다. 바로 이럴 때 필요한 것이 중첩된 JSON 객체 테스트입니다.
복잡한 데이터 구조를 전송하고, 서버가 모든 레벨의 데이터를 올바르게 처리하는지 검증할 수 있습니다.
개요
간단히 말해서, 중첩된 JSON 객체 테스트는 객체 안에 객체가 있고, 배열 안에 객체가 있는 복잡한 데이터 구조를 API에 전송하여 모든 레벨의 데이터가 정확히 처리되는지 확인하는 테스트입니다. 실무에서는 대부분의 비즈니스 도메인이 복잡한 데이터 관계를 가집니다.
전자상거래의 주문(Order > Items > Options), SNS의 게시글(Post > Comments > Replies), 설정(Config > Sections > Fields) 등이 모두 중첩 구조죠. 이런 데이터가 트랜잭션으로 함께 저장되는지 테스트해야 합니다.
기존에는 중첩된 구조를 테스트할 때 Postman에서 JSON을 손으로 타이핑하며 오타를 내곤 했습니다. 이제는 JavaScript 객체 리터럴로 편하게 작성하고, 깊은 레벨의 필드도 toHaveProperty('order.items[0].name') 같은 방식으로 쉽게 검증할 수 있습니다.
핵심 특징은 첫째, JavaScript의 객체와 배열 리터럴을 그대로 사용할 수 있어 가독성이 좋다는 점입니다. 둘째, Supertest가 재귀적으로 JSON 직렬화를 처리하여 깊이 제한이 없습니다.
셋째, Jest의 매처를 사용해 중첩된 구조를 편리하게 검증할 수 있습니다. 이러한 특징들이 복잡한 비즈니스 로직의 정확성을 보장합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('POST /api/orders', () => {
it('should create order with nested structure', async () => {
// 복잡한 중첩 구조의 주문 데이터
const orderData = {
customer: {
name: 'John Doe',
email: 'john@example.com',
phone: '010-1234-5678'
},
shippingAddress: {
street: '123 Main St',
city: 'Seoul',
zipCode: '12345',
country: 'KR'
},
items: [
{
productId: 'prod-001',
name: 'Laptop',
quantity: 1,
price: 1200000,
options: {
color: 'Silver',
memory: '16GB',
storage: '512GB SSD'
}
},
{
productId: 'prod-002',
name: 'Mouse',
quantity: 2,
price: 30000,
options: {
color: 'Black',
wireless: true
}
}
],
payment: {
method: 'card',
cardInfo: {
last4: '1234',
brand: 'Visa'
}
}
};
const response = await request(app)
.post('/api/orders')
.send(orderData)
.expect(201);
// 중첩된 필드 검증
expect(response.body.customer.email).toBe('john@example.com');
expect(response.body.items).toHaveLength(2);
expect(response.body.items[0].options.color).toBe('Silver');
expect(response.body.totalAmount).toBe(1260000); // 계산 검증
});
});
설명
이것이 하는 일: 이 테스트는 주문 생성 API에 고객 정보, 배송지, 여러 상품과 옵션, 결제 정보가 중첩된 복잡한 JSON 구조를 전송하고, 서버가 모든 레벨의 데이터를 올바르게 파싱하여 데이터베이스에 저장하고 총액을 계산하는지 검증합니다. 첫 번째로, orderData 객체를 JavaScript의 객체 리터럴 문법으로 작성합니다.
이렇게 하는 이유는 가독성이 좋고, IDE의 자동완성과 타입 체크를 받을 수 있기 때문입니다. customer, shippingAddress, items, payment가 각각 독립적인 객체이고, items는 배열로 여러 상품을 포함하며, 각 상품은 또 options 객체를 포함하는 3-4단계 중첩 구조입니다.
그 다음으로, Supertest가 이 객체를 JSON.stringify()로 직렬화하고 서버로 전송합니다. 서버의 express.json() 미들웨어는 이를 파싱하여 req.body에 원래의 중첩 구조를 복원합니다.
그러면 비즈니스 로직에서 req.body.customer.email, req.body.items[0].options.color 같은 방식으로 깊은 레벨의 데이터에 접근할 수 있습니다. ORM이나 ODM을 사용한다면 이 구조 그대로 관계형 또는 문서형 데이터베이스에 저장됩니다.
마지막으로, 응답을 검증할 때도 중첩된 경로를 사용합니다. expect(response.body.customer.email)은 customer 객체 안의 email 필드를 확인하고, expect(response.body.items[0].options.color)는 첫 번째 상품의 옵션 객체 안의 color를 검증합니다.
또한 totalAmount가 올바르게 계산되었는지 확인하여 서버가 items 배열을 순회하며 가격을 합산하는 로직이 작동하는지 검증합니다. 여러분이 이 코드를 사용하면 복잡한 비즈니스 트랜잭션을 안전하게 처리할 수 있습니다.
주문, 설문, 설정 같은 복잡한 도메인 모델이 원자적으로 저장되는지 확인하고, 중첩된 각 레벨의 검증이 모두 작동하는지 검증하며, 데이터 무결성을 유지하여 부분적으로만 저장되는 버그를 방지할 수 있습니다.
실전 팁
💡 TypeScript를 사용하면 중첩된 구조의 타입 안정성을 얻을 수 있습니다. 인터페이스를 정의하면 잘못된 필드명이나 타입을 컴파일 시점에 잡을 수 있습니다
💡 중첩 레벨이 깊어지면 테스트 데이터를 별도 파일로 분리하세요. fixtures/orderData.js로 만들면 여러 테스트에서 재사용할 수 있습니다
💡 toMatchObject() 매처를 사용하면 전체 구조를 한 번에 검증할 수 있습니다. 예: expect(response.body).toMatchObject({ customer: { email: expect.any(String) } })
💡 배열 안의 모든 항목을 검증하려면 forEach나 map을 사용하세요. response.body.items.forEach(item => expect(item).toHaveProperty('price'))
💡 순환 참조(circular reference)가 있는 구조는 JSON으로 직렬화할 수 없습니다. 이런 경우 서버에서 순환을 제거하거나 다른 직렬화 방법을 사용해야 합니다
6. 파일 업로드와 JSON 결합
시작하며
여러분이 프로필 사진과 함께 사용자 정보를 업데이트하는 기능을 만들었는데, "이미지는 올라가는데 이름이 안 바뀌어요" 또는 "정보는 바뀌는데 이미지가 안 올라가요"라는 버그를 경험한 적 있나요? 파일과 JSON 데이터를 동시에 보내는 multipart/form-data 요청은 테스트하기 까다롭습니다.
이런 문제는 Content-Type이 multipart/form-data로 바뀌면서 JSON 데이터를 어떻게 인코딩해야 하는지, 파일과 텍스트 필드를 어떻게 함께 처리해야 하는지 몰라서 발생합니다. Postman에서는 form-data 탭을 사용하지만, 코드로는 어떻게 테스트해야 할까요?
바로 이럴 때 필요한 것이 파일 업로드와 JSON 결합 테스트입니다. Supertest의 .attach()와 .field() 메서드로 파일과 데이터를 함께 전송하고, 서버가 모두 올바르게 처리하는지 검증할 수 있습니다.
개요
간단히 말해서, 파일 업로드와 JSON 결합 테스트는 이미지, 문서 같은 파일과 함께 일반 텍스트 데이터를 multipart/form-data 형식으로 전송하여 서버가 파일을 저장하고 메타데이터를 처리하는지 확인하는 테스트입니다. 실무에서는 프로필 업데이트, 게시글 작성(이미지 첨부), 문서 업로드(메타데이터 포함) 등이 모두 이런 형태의 요청을 사용합니다.
사용자는 파일과 정보를 동시에 제출하고, 서버는 파일을 S3나 로컬 스토리지에 저장하면서 관련 데이터를 데이터베이스에 저장해야 하죠. 기존에는 multer 같은 미들웨어를 설정하고 Postman에서 수동으로 파일을 선택해 테스트했습니다.
이제는 .attach('fieldName', 'path/to/file')로 코드에서 파일을 첨부하고, .field()로 텍스트 필드를 추가하여 자동화할 수 있습니다. 핵심 특징은 첫째, .attach() 메서드가 파일 경로나 Buffer를 받아 자동으로 multipart 인코딩을 처리한다는 점입니다.
둘째, .field()로 일반 텍스트 필드를 추가하거나, 복잡한 객체는 JSON.stringify()로 전달할 수 있습니다. 셋째, 여러 파일을 동시에 업로드하는 것도 .attach()를 반복 호출하면 됩니다.
이러한 특징들이 파일 처리 로직의 안정성을 보장합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
const path = require('path');
describe('POST /api/profile/update', () => {
it('should update profile with avatar image and user data', async () => {
const testImagePath = path.join(__dirname, 'fixtures', 'avatar.jpg');
const response = await request(app)
.post('/api/profile/update')
.set('Authorization', 'Bearer valid-token-here')
.attach('avatar', testImagePath) // 파일 첨부
.field('name', 'John Doe') // 텍스트 필드
.field('bio', 'Software Developer') // 텍스트 필드
.field('settings', JSON.stringify({ // 복잡한 객체는 JSON으로
notifications: true,
privacy: 'public'
}))
.expect(200);
// 파일 업로드 확인
expect(response.body.avatarUrl).toMatch(/^https:\/\/.+\/.+\.jpg$/);
// 데이터 업데이트 확인
expect(response.body.name).toBe('John Doe');
expect(response.body.bio).toBe('Software Developer');
// 복잡한 객체 파싱 확인
expect(response.body.settings.notifications).toBe(true);
});
it('should handle multiple file uploads', async () => {
const response = await request(app)
.post('/api/posts')
.attach('images', 'tests/fixtures/image1.jpg') // 첫 번째 파일
.attach('images', 'tests/fixtures/image2.jpg') // 두 번째 파일
.field('title', 'My Post')
.field('content', 'Post content')
.expect(201);
expect(response.body.imageUrls).toHaveLength(2);
});
});
설명
이것이 하는 일: 이 테스트는 프로필 업데이트 API에 아바타 이미지 파일과 함께 이름, 소개, 설정 같은 텍스트 데이터를 multipart/form-data 형식으로 전송하고, 서버가 파일을 저장소에 업로드하면서 사용자 정보를 데이터베이스에 업데이트하는지 검증합니다. 첫 번째로, path.join()을 사용해 테스트 이미지 파일의 경로를 생성합니다.
이렇게 하는 이유는 운영체제마다 경로 구분자가 다르기 때문입니다(Windows는 , Linux/Mac은 /). tests/fixtures/ 디렉토리에 테스트용 이미지를 미리 준비해두고, 이를 반복적으로 사용합니다.
실제 파일이 필요하므로 beforeAll()에서 생성하거나 저장소에 커밋해둡니다. 그 다음으로, .attach('avatar', testImagePath)가 실행되면 Supertest는 파일을 읽어 multipart/form-data의 한 파트로 만듭니다.
Content-Type 헤더는 자동으로 'multipart/form-data; boundary=...'로 설정되고, 각 필드와 파일이 boundary로 구분된 형식으로 인코딩됩니다. .field()로 추가한 텍스트 데이터도 같은 방식으로 인코딩됩니다.
복잡한 객체는 JSON.stringify()로 문자열로 만들어 전달하고, 서버에서 JSON.parse()로 다시 객체로 변환합니다. 마지막으로, 서버는 multer 같은 미들웨어로 파일을 파싱하여 req.file 또는 req.files에 저장하고, 텍스트 필드는 req.body에 저장합니다.
비즈니스 로직에서 파일을 S3나 로컬 디스크에 저장하고 URL을 반환하며, 텍스트 데이터는 데이터베이스를 업데이트하는 데 사용합니다. 테스트는 avatarUrl이 올바른 URL 형식인지, 텍스트 데이터가 정확히 업데이트되었는지, 복잡한 객체가 올바르게 파싱되었는지 모두 검증합니다.
여러분이 이 코드를 사용하면 파일과 데이터가 원자적으로 처리되는지 확인할 수 있습니다. 파일 업로드 실패 시 트랜잭션이 롤백되는지 검증하고, 파일 형식 검증(MIME type, 크기 제한)이 작동하는지 테스트하며, 여러 파일을 동시에 업로드하는 복잡한 시나리오도 자동화할 수 있습니다.
실전 팁
💡 테스트용 파일은 작은 크기로 준비하세요. 1KB 미만의 더미 이미지로도 충분히 로직을 검증할 수 있고, 테스트 속도가 빨라집니다
💡 Buffer를 직접 생성해 사용하면 파일 없이도 테스트할 수 있습니다: .attach('file', Buffer.from('test'), 'test.txt')
💡 파일 형식 검증을 테스트하세요. .txt 파일을 이미지 필드에 업로드하여 서버가 거부하는지 확인합니다
💡 대용량 파일 업로드는 타임아웃을 늘려야 합니다: .timeout(10000) 또는 Jest의 jest.setTimeout()을 사용하세요
💡 S3 같은 외부 스토리지를 사용한다면 테스트 환경에서는 mock을 사용하거나 별도의 테스트 버킷을 사용하여 프로덕션 데이터와 분리하세요
7. 쿼리 파라미터와 본문 결합
시작하며
여러분이 검색 API를 만들었는데, 필터 조건은 쿼리 파라미터로, 복잡한 검색 옵션은 본문으로 받아야 하는 경우를 만난 적 있나요? GET 요청은 본문을 가질 수 없다는 HTTP 규칙 때문에, POST 요청에 쿼리 파라미터와 본문을 함께 사용하는 상황이죠.
이런 문제는 REST API 설계에서 자주 발생합니다. 페이지네이션(page, limit)은 쿼리로, 복잡한 필터 조건(가격 범위, 여러 카테고리, 날짜 범위)은 본문으로 보내는 것이 실용적입니다.
특히 Elasticsearch 같은 검색 엔진을 사용할 때 흔한 패턴입니다. 바로 이럴 때 필요한 것이 쿼리 파라미터와 본문 결합 테스트입니다.
.query()와 .send()를 함께 사용하여 URL의 파라미터와 요청 본문을 동시에 전송하고 검증할 수 있습니다.
개요
간단히 말해서, 쿼리 파라미터와 본문 결합 테스트는 URL의 쿼리 스트링(?page=1&limit=10)과 요청 본문(JSON 필터 조건)을 동시에 전송하여 서버가 두 소스의 파라미터를 모두 올바르게 처리하는지 확인하는 테스트입니다. 실무에서는 검색, 필터링, 대시보드 같은 기능이 이런 형태를 사용합니다.
간단한 파라미터(정렬 순서, 페이지 번호)는 URL에, 복잡한 조건(다중 필터, 날짜 범위)은 본문에 넣는 것이 URL 길이 제한도 피하고 가독성도 좋습니다. 기존에는 쿼리와 본문 중 하나만 테스트하거나, 둘 다 사용할 때 서버에서 어떻게 파싱되는지 헷갈렸습니다.
이제는 .query({ page: 1 }).send({ filters: {...} }) 형태로 명확히 구분하여 전송하고, 서버는 req.query와 req.body에서 각각 접근할 수 있습니다. 핵심 특징은 첫째, .query() 메서드가 객체를 자동으로 쿼리 스트링으로 변환한다는 점입니다(url encoding 포함).
둘째, .send()는 별도로 Content-Type: application/json으로 본문을 전송합니다. 셋째, 두 방식을 혼용해도 서로 간섭하지 않아 안전하게 사용할 수 있습니다.
이러한 특징들이 복잡한 API 설계를 가능하게 합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('POST /api/products/search', () => {
it('should search with query params and body filters', async () => {
// 복잡한 필터 조건
const searchFilters = {
categories: ['electronics', 'computers'],
priceRange: {
min: 100000,
max: 2000000
},
brands: ['Apple', 'Samsung', 'LG'],
attributes: {
color: ['black', 'silver'],
warranty: true
}
};
const response = await request(app)
.post('/api/products/search')
.query({ page: 1, limit: 20, sort: 'price-asc' }) // 쿼리 파라미터
.send(searchFilters) // 본문에 복잡한 필터
.expect(200);
// 페이지네이션 확인
expect(response.body.page).toBe(1);
expect(response.body.limit).toBe(20);
expect(response.body.results).toHaveLength(20);
// 필터링 결과 확인
response.body.results.forEach(product => {
expect(product.price).toBeGreaterThanOrEqual(100000);
expect(product.price).toBeLessThanOrEqual(2000000);
expect(['electronics', 'computers']).toContain(product.category);
});
});
it('should handle array query params', async () => {
const response = await request(app)
.post('/api/products/search')
.query({ tags: ['new', 'sale'], region: 'seoul' }) // 배열 쿼리
.send({ keyword: 'laptop' })
.expect(200);
expect(response.body.appliedFilters.tags).toEqual(['new', 'sale']);
});
});
설명
이것이 하는 일: 이 테스트는 상품 검색 API에 페이지네이션과 정렬 정보는 쿼리 파라미터로, 카테고리/가격/브랜드 같은 복잡한 필터 조건은 JSON 본문으로 전송하고, 서버가 두 소스의 데이터를 결합하여 올바른 검색 결과를 반환하는지 검증합니다. 첫 번째로, .query({ page: 1, limit: 20, sort: 'price-asc' })를 호출하면 Supertest는 이를 '?page=1&limit=20&sort=price-asc' 형태의 쿼리 스트링으로 변환하여 URL에 추가합니다.
이렇게 하는 이유는 페이지네이션과 정렬 같은 메타 정보는 캐싱, 북마킹, 로깅이 쉽도록 URL에 포함시키는 것이 RESTful 하기 때문입니다. 서버에서 req.query.page로 접근할 수 있습니다.
그 다음으로, .send(searchFilters)가 실행되면 복잡한 중첩 객체가 JSON으로 직렬화되어 요청 본문에 포함됩니다. 이 데이터는 req.body로 접근할 수 있으며, categories 배열, priceRange 객체, attributes 중첩 객체 등 모든 구조가 보존됩니다.
서버의 비즈니스 로직은 req.query에서 페이지네이션을, req.body에서 필터를 꺼내 데이터베이스 쿼리를 구성합니다. 마지막으로, 응답을 검증할 때 두 소스의 파라미터가 모두 적용되었는지 확인합니다.
response.body.page와 limit을 체크하여 쿼리 파라미터가 작동했는지 확인하고, forEach로 각 결과 상품이 필터 조건을 만족하는지 검증합니다. 가격이 범위 안에 있는지, 카테고리가 요청한 것 중 하나인지 등을 체크하여 서버가 req.query와 req.body를 모두 고려했음을 입증합니다.
여러분이 이 코드를 사용하면 복잡한 검색 기능을 완전히 테스트할 수 있습니다. URL 길이 제한(보통 2048자)을 걱정하지 않고 복잡한 필터를 전송할 수 있고, 쿼리와 본문의 파라미터가 충돌하지 않는지 확인하며, 캐싱 전략(쿼리 기반)과 필터링 로직(본문 기반)을 독립적으로 관리할 수 있습니다.
실전 팁
💡 쿼리 파라미터에 배열을 전달할 때 서버 파싱 방식을 확인하세요. ?tags=new&tags=sale vs ?tags=new,sale 형식이 다를 수 있습니다
💡 쿼리 파라미터는 항상 문자열로 전달됩니다. 서버에서 parseInt()나 Boolean() 변환이 필요하므로, 이 변환 로직도 테스트하세요
💡 GET 요청에도 본문을 보낼 수 있지만(기술적으로 가능), HTTP 스펙상 권장되지 않습니다. 검색은 POST를 사용하는 것이 안전합니다
💡 쿼리 파라미터에 특수문자(공백, &, = 등)가 있으면 자동으로 URL 인코딩됩니다. decodeURIComponent()로 디코딩하는 로직도 테스트하세요
💡 Elasticsearch를 사용한다면 본문의 필터 구조를 ES 쿼리 DSL 형식에 가깝게 설계하면 변환 로직이 간단해집니다
8. 응답 상태 코드 검증
시작하며
여러분이 API를 만들고 "잘 되는 것 같은데..." 하고 넘어갔다가, 나중에 클라이언트가 에러를 제대로 처리 못해서 앱이 크래시 나는 상황을 겪어본 적 있나요? 성공(200)만 테스트하고 실패 케이스는 놓치는 경우가 많죠.
이런 문제는 HTTP 상태 코드를 제대로 사용하지 않아서 발생합니다. 모든 에러를 500으로 반환하거나, 인증 실패를 403 대신 401로 보내거나, 리소스가 없을 때 200과 빈 배열을 보내는 등의 실수가 있습니다.
클라이언트는 상태 코드를 보고 어떻게 처리할지 결정하므로 정확해야 합니다. 바로 이럴 때 필요한 것이 응답 상태 코드 검증 테스트입니다.
성공뿐 아니라 다양한 실패 시나리오에서 올바른 상태 코드(400, 401, 403, 404, 409, 500 등)가 반환되는지 체계적으로 테스트할 수 있습니다.
개요
간단히 말해서, 응답 상태 코드 검증 테스트는 API가 다양한 상황(성공, 클라이언트 에러, 서버 에러)에서 HTTP 표준에 맞는 올바른 상태 코드를 반환하는지 확인하는 테스트입니다. 실무에서는 상태 코드가 클라이언트의 에러 처리 로직을 결정합니다.
401이면 로그인 페이지로 리다이렉트, 403이면 권한 없음 메시지 표시, 404면 "찾을 수 없음" UI, 500이면 "서버 오류" 페이지를 보여주죠. 잘못된 상태 코드는 잘못된 사용자 경험으로 이어집니다.
기존에는 성공 케이스만 테스트하고 에러는 수동으로 확인했습니다. 이제는 각 에러 타입별로 테스트 케이스를 작성하여 모든 상태 코드가 올바른지 자동으로 검증할 수 있습니다.
.expect(200), .expect(400), .expect(404) 같은 간단한 매처로 쉽게 체크할 수 있죠. 핵심 특징은 첫째, .expect(statusCode) 하나로 간단히 검증할 수 있다는 점입니다.
둘째, 잘못된 상태 코드가 반환되면 테스트가 즉시 실패하여 문제를 발견할 수 있습니다. 셋째, 상태 코드별로 테스트를 그룹화하면 API의 모든 엣지 케이스를 문서화하는 효과도 있습니다.
이러한 특징들이 클라이언트 개발자와의 계약을 명확히 합니다.
코드 예제
const request = require('supertest');
const app = require('../app');
describe('HTTP Status Code Validation', () => {
// 2xx: 성공
it('should return 200 for successful GET', async () => {
await request(app)
.get('/api/users/123')
.expect(200);
});
it('should return 201 for successful resource creation', async () => {
await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201);
});
it('should return 204 for successful deletion', async () => {
await request(app)
.delete('/api/users/123')
.expect(204);
});
// 4xx: 클라이언트 에러
it('should return 400 for invalid request body', async () => {
await request(app)
.post('/api/users')
.send({ invalid: 'data' }) // 필수 필드 누락
.expect(400);
});
it('should return 401 for missing authentication', async () => {
await request(app)
.get('/api/profile')
// Authorization 헤더 없음
.expect(401);
});
it('should return 403 for insufficient permissions', async () => {
await request(app)
.delete('/api/admin/users/123')
.set('Authorization', 'Bearer user-token') // 일반 사용자 토큰
.expect(403); // 관리자 권한 필요
});
it('should return 404 for non-existent resource', async () => {
await request(app)
.get('/api/users/999999') // 존재하지 않는 ID
.expect(404);
});
it('should return 409 for duplicate resource', async () => {
await request(app)
.post('/api/users')
.send({ email: 'existing@test.com' }) // 이미 존재하는 이메일
.expect(409); // 중복 충돌
});
// 5xx: 서버 에러
it('should return 500 for server error', async () => {
// 데이터베이스 연결 실패 시뮬레이션
await request(app)
.get('/api/error-simulation')
.expect(500);
});
});
설명
이것이 하는 일: 이 테스트들은 API가 다양한 상황에서 HTTP 표준에 맞는 적절한 상태 코드를 반환하는지 체계적으로 검증합니다. 성공 시 200/201/204, 클라이언트 에러 시 400/401/403/404/409, 서버 에러 시 500을 반환해야 합니다.
첫 번째로, 2xx 성공 코드를 테스트합니다. 200 OK는 일반적인 성공, 201 Created는 리소스 생성 성공, 204 No Content는 삭제 성공을 의미합니다.
이렇게 하는 이유는 클라이언트가 상태 코드를 보고 응답 본문을 파싱할지 결정하기 때문입니다. 예를 들어 204는 응답 본문이 없으므로 JSON 파싱을 시도하지 않아야 합니다.
그 다음으로, 4xx 클라이언트 에러 코드를 상황별로 테스트합니다. 400 Bad Request는 잘못된 요청 형식, 401 Unauthorized는 인증 정보 없음, 403 Forbidden은 권한 부족, 404 Not Found는 리소스 없음, 409 Conflict는 리소스 충돌(중복 이메일 등)을 의미합니다.
각 에러 타입을 구분하는 것이 중요한데, 예를 들어 401과 403을 혼동하면 클라이언트가 로그인 페이지로 보내야 할지 "권한 없음" 메시지를 보여야 할지 판단할 수 없습니다. 마지막으로, 5xx 서버 에러 코드를 테스트합니다.
500 Internal Server Error는 서버 측 예외, 데이터베이스 연결 실패, 예상치 못한 에러를 의미합니다. 클라이언트는 이 코드를 받으면 재시도하거나 "일시적인 오류" 메시지를 표시합니다.
중요한 점은 클라이언트 잘못이 아니므로 사용자에게 "잘못 입력하셨습니다" 대신 "서버에 문제가 발생했습니다"를 보여야 한다는 것입니다. 여러분이 이 코드를 사용하면 HTTP 표준을 준수하는 API를 만들 수 있습니다.
클라이언트 개발자가 상태 코드만 보고 에러 처리를 구현할 수 있고, API 게이트웨이나 로드 밸런서가 상태 코드를 기반으로 재시도/서킷 브레이커를 적용할 수 있으며, 모니터링 도구가 4xx와 5xx를 구분하여 경보를 발생시킬 수 있습니다.
실전 팁
💡 2xx 범위 전체를 수락하려면 .expect(200)을 여러 번 쓰지 말고, 응답 코드를 직접 체크하세요: expect(response.status).toBeGreaterThanOrEqual(200)
💡 422 Unprocessable Entity는 문법적으로는 맞지만 의미적으로 틀린 요청에 사용합니다. 400과 구분하여 더 명확한 에러 처리가 가능합니다
💡 rate limiting을 구현했다면 429 Too Many Requests도 테스트하세요. Retry-After 헤더도 함께 반환하는지 확인합니다
💡 3xx 리다이렉트 코드도 테스트하세요. 301/302/307은 .expect(302).expect('Location', '/new-url') 형태로 검증합니다
💡 커스텀 에러 핸들러를 만들어 예외를 적절한 상태 코드로 변환하세요. try-catch에서 모든 에러를 500으로 보내면 클라이언트가 대응하기 어렵습니다