이미지 로딩 중...
AI Generated
2025. 11. 13. · 3 Views
쿼리 파라미터와 경로 파라미터 테스트 완벽 가이드
API 테스트에서 가장 중요한 쿼리 파라미터와 경로 파라미터 테스트 방법을 초급자도 쉽게 이해할 수 있도록 설명합니다. Jest와 Supertest를 활용한 실전 예제와 함께 실무에서 바로 활용할 수 있는 팁을 제공합니다.
목차
- 쿼리 파라미터 기본 테스트 - GET 요청의 핵심
- 경로 파라미터 기본 테스트 - 리소스 식별의 핵심
- 복합 파라미터 테스트 - 쿼리와 경로를 함께 사용하기
- 배열 타입 쿼리 파라미터 테스트 - 다중 선택 필터링
- 선택적 쿼리 파라미터 테스트 - 기본값과 유연성
- 잘못된 파라미터 검증 테스트 - 에러 처리의 중요성
- 중첩된 경로 파라미터 테스트 - 계층적 리소스 관리
- PUT DELETE 요청의 파라미터 테스트 - 수정과 삭제 검증
1. 쿼리 파라미터 기본 테스트 - GET 요청의 핵심
시작하며
여러분이 사용자 검색 API를 만들었는데, 검색어나 페이지 번호가 제대로 전달되지 않아서 엉뚱한 결과가 나온 적 있나요? 예를 들어 /users?name=john&age=25 같은 URL에서 name과 age 값이 제대로 서버에 전달되는지 확인하지 않았다면, 실제 서비스에서 큰 문제가 발생할 수 있습니다.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 쿼리 파라미터는 URL의 ?
뒤에 붙는 키-값 쌍으로, 필터링, 검색, 정렬 등에 광범위하게 사용됩니다. 하지만 많은 개발자들이 이를 제대로 테스트하지 않아서 배포 후에 버그를 발견하게 됩니다.
바로 이럴 때 필요한 것이 쿼리 파라미터 테스트입니다. 자동화된 테스트를 통해 여러분의 API가 쿼리 파라미터를 정확하게 받아서 처리하는지 검증할 수 있습니다.
개요
간단히 말해서, 쿼리 파라미터 테스트는 URL의 쿼리 스트링이 서버에서 올바르게 파싱되고 처리되는지 확인하는 작업입니다. 실무에서는 검색 기능, 필터링, 페이지네이션, 정렬 등 거의 모든 GET 요청에서 쿼리 파라미터를 사용합니다.
예를 들어, 전자상거래 사이트에서 "가격이 10,000원에서 50,000원 사이이고, 카테고리가 '의류'인 상품을 평점 순으로 정렬" 같은 복잡한 필터링이 모두 쿼리 파라미터로 처리됩니다. 기존에는 Postman 같은 도구로 수동으로 테스트했다면, 이제는 Jest와 Supertest를 사용해서 자동화된 테스트를 작성할 수 있습니다.
코드가 변경될 때마다 자동으로 테스트가 실행되어 버그를 조기에 발견할 수 있습니다. 쿼리 파라미터 테스트의 핵심 특징은 첫째, 여러 파라미터를 동시에 전달하는 케이스를 테스트해야 하고, 둘째, 선택적(optional) 파라미터와 필수(required) 파라미터를 구분해서 테스트해야 하며, 셋째, 잘못된 값이나 타입이 들어왔을 때의 에러 처리도 검증해야 합니다.
이러한 특징들을 이해하면 견고한 API를 만들 수 있습니다.
코드 예제
// 사용자 검색 API 테스트 - 쿼리 파라미터로 이름과 나이 필터링
const request = require('supertest');
const app = require('../app');
describe('GET /users - 쿼리 파라미터 테스트', () => {
it('이름과 나이로 사용자를 필터링해야 함', async () => {
// given: 테스트용 쿼리 파라미터 준비
const queryParams = { name: 'john', age: 25 };
// when: 쿼리 파라미터와 함께 GET 요청
const response = await request(app)
.get('/users')
.query(queryParams) // .query()로 쿼리 파라미터 전달
.expect(200);
// then: 응답이 필터 조건과 일치하는지 검증
expect(response.body).toHaveProperty('users');
expect(response.body.users).toBeInstanceOf(Array);
expect(response.body.users[0].name).toBe('john');
expect(response.body.users[0].age).toBe(25);
});
});
설명
이것이 하는 일: 위 테스트 코드는 사용자 검색 API가 쿼리 파라미터로 전달받은 이름과 나이를 올바르게 처리하는지 검증합니다. 첫 번째로, queryParams 객체를 만들어서 테스트할 파라미터를 정의합니다.
이렇게 객체로 만들면 코드가 깔끔해지고, 나중에 파라미터를 추가하거나 수정하기 쉽습니다. 실제로는 { name: 'john', age: 25 }가 URL에서 ?name=john&age=25로 변환됩니다.
두 번째로, request(app).get('/users').query(queryParams)를 실행하면서 쿼리 파라미터를 전달합니다. Supertest의 .query() 메서드는 객체를 받아서 자동으로 URL 쿼리 스트링으로 변환해줍니다.
이 부분이 핵심인데, 수동으로 '/users?name=john&age=25' 같이 문자열을 만들 필요가 없어서 실수를 줄일 수 있습니다. 세 번째로, .expect(200)으로 HTTP 상태 코드를 확인한 후, response.body의 내용을 검증합니다.
여기서는 배열 형태의 사용자 목록이 반환되고, 첫 번째 사용자의 이름과 나이가 우리가 요청한 값과 일치하는지 확인합니다. 여러분이 이 코드를 사용하면 API가 쿼리 파라미터를 제대로 파싱하고 필터링 로직이 정확하게 동작하는지 자동으로 검증할 수 있습니다.
또한 코드 리팩토링이나 데이터베이스 쿼리 수정 후에도 기능이 정상적으로 작동하는지 빠르게 확인할 수 있어서 배포 전 자신감을 가질 수 있습니다.
실전 팁
💡 쿼리 파라미터가 없을 때의 동작도 테스트하세요. 모든 사용자를 반환하거나 기본값을 사용하는 등 예상된 동작을 검증해야 합니다.
💡 특수문자나 공백이 포함된 쿼리 값도 테스트하세요. 예를 들어 name: 'John Doe'처럼 공백이 있는 경우 URL 인코딩이 제대로 되는지 확인이 필요합니다.
💡 타입 검증도 중요합니다. age는 숫자여야 하는데 문자열로 들어왔을 때 에러 처리가 되는지 테스트하세요.
💡 여러 파라미터를 조합한 테스트 케이스를 만드세요. 실무에서는 단일 파라미터보다 복합 조건이 훨씬 많습니다.
💡 페이지네이션 파라미터(page, limit)는 경계값 테스트가 필수입니다. page=0, page=-1, limit=0 같은 엣지 케이스를 꼭 확인하세요.
2. 경로 파라미터 기본 테스트 - 리소스 식별의 핵심
시작하며
여러분이 특정 사용자의 프로필을 조회하는 API를 만들었는데, /users/123 같은 요청에서 123이라는 ID가 제대로 전달되지 않아서 엉뚱한 사용자 정보가 나온 적 있나요? 또는 존재하지 않는 ID로 요청했을 때 적절한 404 에러를 반환하지 않아서 클라이언트가 혼란스러워한 경험이 있으신가요?
이런 문제는 REST API에서 매우 흔합니다. 경로 파라미터는 URL 경로의 일부로 포함되어 특정 리소스를 식별하는 데 사용됩니다.
쿼리 파라미터가 선택적인 필터링에 사용된다면, 경로 파라미터는 필수적인 리소스 식별에 사용되기 때문에 더욱 정확한 테스트가 필요합니다. 바로 이럴 때 필요한 것이 경로 파라미터 테스트입니다.
특정 리소스에 대한 CRUD 작업이 올바르게 동작하는지 검증하고, 잘못된 ID나 존재하지 않는 리소스에 대한 에러 처리도 확인할 수 있습니다.
개요
간단히 말해서, 경로 파라미터 테스트는 URL 경로에 포함된 변수(주로 ID)가 서버에서 올바르게 추출되고 해당 리소스를 정확하게 조회하거나 조작하는지 확인하는 작업입니다. 실무에서는 RESTful API의 핵심 패턴으로 사용됩니다.
예를 들어, /users/:id, /posts/:postId/comments/:commentId 같은 패턴에서 콜론(:) 뒤의 부분이 경로 파라미터입니다. 전자상거래 사이트에서 "특정 주문 상세 조회", "특정 상품 수정", "특정 리뷰 삭제" 같은 모든 작업이 경로 파라미터를 통해 대상을 식별합니다.
기존에는 하드코딩된 ID로만 테스트했다면, 이제는 다양한 ID 형식(숫자, UUID, MongoDB ObjectId 등)과 유효하지 않은 ID에 대한 테스트까지 자동화할 수 있습니다. 특히 경로 파라미터는 필수값이기 때문에 누락되거나 잘못된 형식일 때의 에러 처리가 매우 중요합니다.
경로 파라미터 테스트의 핵심 특징은 첫째, 유효한 ID로 리소스를 정확하게 조회하는지 확인하고, 둘째, 존재하지 않는 ID에 대해 404 에러를 반환하는지 검증하며, 셋째, 잘못된 형식의 ID(예: 문자열을 숫자 ID로 사용)에 대한 400 에러 처리를 확인해야 합니다. 이러한 특징들을 모두 테스트하면 API의 신뢰성이 크게 향상됩니다.
코드 예제
// 특정 사용자 조회 API 테스트 - 경로 파라미터로 ID 전달
const request = require('supertest');
const app = require('../app');
describe('GET /users/:id - 경로 파라미터 테스트', () => {
it('유효한 ID로 특정 사용자를 조회해야 함', async () => {
// given: 테스트용 사용자 ID
const userId = 123;
// when: 경로 파라미터에 ID를 포함해서 GET 요청
const response = await request(app)
.get(`/users/${userId}`) // 템플릿 리터럴로 ID를 경로에 삽입
.expect(200);
// then: 응답이 요청한 사용자의 정보인지 검증
expect(response.body).toHaveProperty('id', userId);
expect(response.body).toHaveProperty('name');
expect(response.body).toHaveProperty('email');
});
it('존재하지 않는 ID는 404를 반환해야 함', async () => {
// when & then: 존재하지 않는 ID로 요청시 404 에러
await request(app)
.get('/users/99999')
.expect(404);
});
});
설명
이것이 하는 일: 위 테스트 코드는 사용자 상세 조회 API가 경로 파라미터로 전달받은 ID를 올바르게 처리하고, 존재하지 않는 ID에 대해 적절한 에러를 반환하는지 검증합니다. 첫 번째 테스트 케이스에서는 userId 변수를 정의하고, 템플릿 리터럴(백틱)을 사용해서 /users/${userId} 형태로 동적 URL을 생성합니다.
이 방법이 중요한 이유는 여러 테스트에서 다른 ID를 쉽게 사용할 수 있고, 변수명을 통해 코드의 의도가 명확해지기 때문입니다. Express 같은 프레임워크는 자동으로 URL에서 이 값을 추출해서 req.params.id로 접근할 수 있게 해줍니다.
두 번째로, .expect(200)으로 성공 응답을 확인한 후, response.body에 우리가 요청한 사용자의 정보가 담겨있는지 검증합니다. 특히 toHaveProperty('id', userId)는 응답의 id 필드가 우리가 요청한 userId와 정확히 일치하는지 확인하는데, 이는 서버가 올바른 사용자를 조회했다는 강력한 증거입니다.
세 번째로, 두 번째 테스트 케이스에서는 존재하지 않는 ID(99999)로 요청했을 때 404 Not Found 에러가 반환되는지 확인합니다. 이는 매우 중요한 테스트인데, 실제 서비스에서 클라이언트가 잘못된 ID로 요청했을 때 명확한 에러 메시지를 받아야 적절한 처리를 할 수 있기 때문입니다.
여러분이 이 패턴을 사용하면 단일 리소스에 대한 모든 CRUD 작업(조회, 수정, 삭제)을 체계적으로 테스트할 수 있습니다. 또한 다양한 ID 타입(정수, UUID, ObjectId)에 대해서도 같은 방식으로 테스트를 작성할 수 있어서 코드의 일관성이 유지됩니다.
특히 존재하지 않는 리소스에 대한 에러 처리를 테스트하면 프론트엔드 개발자가 안심하고 API를 사용할 수 있습니다.
실전 팁
💡 경로 파라미터의 타입 검증을 반드시 테스트하세요. 숫자 ID를 기대하는데 문자열이 들어오면 400 Bad Request를 반환해야 합니다.
💡 UUID나 ObjectId 같은 복잡한 ID 형식을 사용한다면, 잘못된 형식의 ID에 대한 테스트도 추가하세요.
💡 여러 개의 경로 파라미터가 있는 경우(/posts/:postId/comments/:commentId) 모든 조합을 테스트하세요.
💡 경로 파라미터를 사용하는 모든 HTTP 메서드(GET, PUT, DELETE)에 대해 테스트를 작성하세요. 메서드마다 다른 동작을 하기 때문입니다.
💡 권한 검증도 함께 테스트하세요. 다른 사용자의 리소스에 접근하려 할 때 403 Forbidden을 반환하는지 확인이 필요합니다.
3. 복합 파라미터 테스트 - 쿼리와 경로를 함께 사용하기
시작하며
여러분이 특정 게시글의 댓글 목록을 조회하는 API를 만들었는데, 게시글 ID는 경로에, 댓글 필터링 조건은 쿼리에 넣어야 하는 복잡한 상황을 경험해본 적 있나요? 예를 들어 /posts/123/comments?sortBy=date&order=desc 같은 URL에서 게시글 ID(123)와 정렬 옵션(sortBy, order)을 동시에 처리해야 하는 경우입니다.
이런 패턴은 실무에서 매우 자주 사용됩니다. 경로 파라미터로 부모 리소스를 식별하고, 쿼리 파라미터로 자식 리소스의 필터링, 정렬, 페이지네이션을 처리하는 것이 REST API의 일반적인 설계 패턴입니다.
하지만 두 가지를 동시에 테스트하지 않으면 어느 부분에서 문제가 발생했는지 찾기 어렵습니다. 바로 이럴 때 필요한 것이 복합 파라미터 테스트입니다.
경로 파라미터와 쿼리 파라미터를 함께 사용하는 복잡한 API도 체계적으로 검증할 수 있습니다.
개요
간단히 말해서, 복합 파라미터 테스트는 경로 파라미터로 대상 리소스를 식별하고, 쿼리 파라미터로 추가 조건을 지정하는 API의 동작을 검증하는 작업입니다. 실무에서는 계층적 리소스 구조에서 필수적으로 사용됩니다.
예를 들어, 소셜 미디어 앱에서 "특정 사용자(경로)가 작성한 게시글 중 최근 30일(쿼리) 것만 좋아요 순(쿼리)으로 정렬" 같은 복잡한 요구사항이 모두 이 패턴으로 구현됩니다. 전자상거래에서도 "특정 카테고리(경로)의 상품 중 가격대(쿼리)와 브랜드(쿼리)로 필터링"하는 식으로 사용됩니다.
기존에는 경로 파라미터와 쿼리 파라미터를 따로따로 테스트했다면, 이제는 두 가지가 함께 작동하는 통합 시나리오를 테스트할 수 있습니다. 이렇게 하면 각각은 정상이지만 함께 사용했을 때 발생하는 버그를 찾아낼 수 있습니다.
복합 파라미터 테스트의 핵심은 첫째, 경로 파라미터가 올바른 부모 리소스를 식별하는지 확인하고, 둘째, 쿼리 파라미터가 해당 리소스 범위 내에서만 필터링을 적용하는지 검증하며, 셋째, 잘못된 조합(예: 존재하지 않는 부모 리소스에 쿼리 적용)에 대한 에러 처리를 확인해야 합니다. 이를 통해 복잡한 API도 안전하게 구현할 수 있습니다.
코드 예제
// 특정 게시글의 댓글 목록 조회 - 경로와 쿼리 파라미터 동시 사용
const request = require('supertest');
const app = require('../app');
describe('GET /posts/:postId/comments - 복합 파라미터 테스트', () => {
it('특정 게시글의 댓글을 정렬 옵션과 함께 조회해야 함', async () => {
// given: 게시글 ID와 쿼리 옵션 준비
const postId = 456;
const queryOptions = { sortBy: 'date', order: 'desc', limit: 10 };
// when: 경로 파라미터와 쿼리 파라미터를 함께 전달
const response = await request(app)
.get(`/posts/${postId}/comments`) // 경로에 postId 포함
.query(queryOptions) // 쿼리 옵션 추가
.expect(200);
// then: 응답이 해당 게시글의 댓글이고 정렬되었는지 검증
expect(response.body.comments).toBeInstanceOf(Array);
expect(response.body.comments.length).toBeLessThanOrEqual(10);
expect(response.body.comments[0].postId).toBe(postId);
// 날짜 내림차순 정렬 검증
if (response.body.comments.length > 1) {
const firstDate = new Date(response.body.comments[0].createdAt);
const secondDate = new Date(response.body.comments[1].createdAt);
expect(firstDate.getTime()).toBeGreaterThanOrEqual(secondDate.getTime());
}
});
});
설명
이것이 하는 일: 위 테스트 코드는 특정 게시글의 댓글 목록 API가 경로 파라미터로 게시글을 식별하고, 쿼리 파라미터로 정렬과 제한 조건을 적용하는지 검증합니다. 첫 번째로, postId와 queryOptions를 각각 준비합니다.
이렇게 분리해서 정의하면 어떤 값이 경로로 가고 어떤 값이 쿼리로 가는지 명확하게 구분됩니다. 실제 URL은 /posts/456/comments?sortBy=date&order=desc&limit=10으로 생성되는데, Supertest가 자동으로 처리해줍니다.
두 번째로, .get() 메서드에는 템플릿 리터럴로 만든 경로를 전달하고, 바로 뒤에 .query(queryOptions)를 체이닝합니다. 이 패턴이 중요한데, 경로와 쿼리를 각각의 메서드로 분리해서 전달하면 코드의 의도가 명확해지고 실수를 줄일 수 있습니다.
세 번째로, 응답 검증에서는 여러 레이어를 확인합니다. 먼저 배열 형태인지, 개수 제한이 적용되었는지 확인하고, comments[0].postId가 우리가 요청한 postId와 일치하는지 확인합니다.
이는 매우 중요한데, 다른 게시글의 댓글이 섞여 나오는 버그를 방지할 수 있습니다. 마지막으로, 정렬이 실제로 적용되었는지 확인하기 위해 첫 번째와 두 번째 댓글의 날짜를 비교합니다.
내림차순 정렬을 요청했으므로 첫 번째 댓글의 날짜가 두 번째보다 최근이거나 같아야 합니다. 이런 세밀한 검증이 API의 품질을 크게 향상시킵니다.
여러분이 이 패턴을 사용하면 복잡한 계층 구조의 API도 체계적으로 테스트할 수 있습니다. 특히 마이크로서비스 아키텍처에서 여러 리소스가 중첩된 구조를 가질 때 매우 유용하며, 각 파라미터 타입의 역할을 명확히 하여 API 설계의 일관성도 유지할 수 있습니다.
실전 팁
💡 경로 파라미터가 유효하지 않을 때 쿼리 파라미터는 무시되어야 합니다. 존재하지 않는 게시글의 댓글을 조회하면 404를 반환해야 합니다.
💡 쿼리 파라미터의 기본값 동작도 테스트하세요. sortBy를 생략하면 어떤 순서로 정렬되는지 명확해야 합니다.
💡 페이지네이션과 필터링을 함께 사용할 때는 전체 개수(totalCount)도 검증하세요. 필터링 후의 개수가 맞는지 확인이 필요합니다.
💡 쿼리 파라미터의 값이 경로 파라미터와 충돌하지 않는지 확인하세요. 예를 들어 쿼리에 postId가 또 있으면 안 됩니다.
💡 복잡한 쿼리 조합을 테스트할 때는 테스트 이름을 구체적으로 작성하세요. "정렬, 필터링, 페이지네이션을 동시에 적용"처럼 명확하게 표현하세요.
4. 배열 타입 쿼리 파라미터 테스트 - 다중 선택 필터링
시작하며
여러분이 상품 검색 기능을 만들었는데, 사용자가 여러 카테고리나 여러 브랜드를 동시에 선택해서 필터링하고 싶어할 때 어떻게 처리하셨나요? 예를 들어 /products?category=electronics&category=books&category=sports 같은 URL에서 같은 키가 여러 번 반복되는 경우를 제대로 테스트하지 않으면 실제 서비스에서 필터가 작동하지 않을 수 있습니다.
이런 문제는 전자상거래, 검색 엔진, 필터링 기능이 있는 모든 애플리케이션에서 발생합니다. 단일 값만 다루는 테스트는 흔하지만, 배열 형태의 쿼리 파라미터를 제대로 테스트하는 경우는 드뭅니다.
하지만 실제 사용자는 다중 선택을 매우 자주 사용하기 때문에 이를 검증하지 않으면 사용자 경험이 크게 나빠집니다. 바로 이럴 때 필요한 것이 배열 타입 쿼리 파라미터 테스트입니다.
같은 키를 여러 번 사용하거나 배열 형태로 전달되는 값들을 API가 올바르게 처리하는지 검증할 수 있습니다.
개요
간단히 말해서, 배열 타입 쿼리 파라미터 테스트는 같은 키를 여러 개의 값과 함께 전달했을 때 서버가 배열로 인식하고 올바르게 필터링을 적용하는지 확인하는 작업입니다. 실무에서는 다중 선택 필터가 필요한 모든 곳에서 사용됩니다.
예를 들어, 채용 플랫폼에서 "여러 직무 + 여러 지역 + 여러 경력"을 동시에 선택하거나, 음악 스트리밍 서비스에서 "여러 장르 + 여러 아티스트"로 필터링할 때 모두 배열 타입 쿼리 파라미터가 사용됩니다. 기존에는 단일 카테고리만 테스트했다면, 이제는 여러 카테고리를 선택했을 때 OR 조건으로 동작하는지(카테고리 A 또는 B), AND 조건으로 동작하는지(태그 A와 B 모두 포함), 아니면 더 복잡한 로직인지 명확히 테스트할 수 있습니다.
배열 타입 쿼리 파라미터의 핵심 특징은 첫째, 여러 형식을 지원해야 합니다(?id=1&id=2 또는 ?id=1,2 또는 ?id[]=1&id[]=2), 둘째, 빈 배열이나 단일 값도 정상 처리되어야 하며, 셋째, 배열의 각 요소에 대한 타입 검증과 중복 제거 같은 추가 로직도 테스트해야 합니다.
코드 예제
// 여러 카테고리로 상품 필터링 - 배열 타입 쿼리 파라미터
const request = require('supertest');
const app = require('../app');
describe('GET /products - 배열 쿼리 파라미터 테스트', () => {
it('여러 카테고리를 동시에 필터링해야 함', async () => {
// given: 여러 카테고리를 배열로 준비
const categories = ['electronics', 'books', 'sports'];
// when: 배열을 쿼리 파라미터로 전달
const response = await request(app)
.get('/products')
.query({ category: categories }) // Supertest가 자동으로 category=electronics&category=books&category=sports로 변환
.expect(200);
// then: 응답의 모든 상품이 선택한 카테고리 중 하나에 속하는지 검증
expect(response.body.products).toBeInstanceOf(Array);
expect(response.body.products.length).toBeGreaterThan(0);
response.body.products.forEach(product => {
expect(categories).toContain(product.category); // OR 조건: 카테고리 중 하나에 속함
});
});
it('배열과 다른 쿼리를 함께 사용할 수 있어야 함', async () => {
// when: 배열 쿼리와 일반 쿼리를 동시에 전달
const response = await request(app)
.get('/products')
.query({ category: ['electronics', 'books'], minPrice: 10000 })
.expect(200);
// then: 카테고리와 가격 조건이 모두 적용되었는지 검증
response.body.products.forEach(product => {
expect(['electronics', 'books']).toContain(product.category);
expect(product.price).toBeGreaterThanOrEqual(10000);
});
});
});
설명
이것이 하는 일: 위 테스트 코드는 상품 검색 API가 여러 카테고리를 동시에 필터로 받아서 OR 조건으로 검색하는지, 그리고 다른 쿼리 파라미터와 함께 사용할 수 있는지 검증합니다. 첫 번째 테스트에서는 categories 배열을 정의하고 .query({ category: categories })로 전달합니다.
Supertest는 이를 category=electronics&category=books&category=sports 형태로 자동 변환하는데, 이는 HTTP 표준에서 배열을 표현하는 일반적인 방법입니다. Express에서는 req.query.category가 자동으로 배열 ['electronics', 'books', 'sports']로 파싱됩니다.
두 번째로, 응답 검증에서 forEach를 사용해서 모든 상품의 카테고리가 우리가 요청한 카테고리 목록에 포함되는지 확인합니다. toContain() matcher를 사용하면 OR 조건을 깔끔하게 검증할 수 있습니다.
이는 "electronics 또는 books 또는 sports" 중 하나라도 일치하면 통과하는 로직입니다. 세 번째로, 두 번째 테스트 케이스에서는 배열 쿼리(category)와 일반 쿼리(minPrice)를 함께 사용합니다.
이는 실무에서 매우 흔한 패턴인데, "여러 카테고리 중 하나이면서 동시에 최소 가격 조건을 만족"하는 복합 필터를 구현합니다. 검증에서도 두 조건을 모두 확인합니다.
여러분이 이 패턴을 사용하면 복잡한 다중 선택 UI를 백엔드에서 안전하게 지원할 수 있습니다. 프론트엔드에서 체크박스나 멀티 셀렉트로 여러 항목을 선택했을 때, 백엔드가 정확히 원하는 결과를 반환한다는 확신을 가질 수 있습니다.
또한 배열이 비어있거나 단일 값일 때도 동일한 로직으로 처리되는지 추가 테스트를 작성하면 더욱 견고한 API가 됩니다.
실전 팁
💡 빈 배열(category: [])을 전달했을 때의 동작을 명확히 정의하고 테스트하세요. 모든 항목을 반환할지, 아무것도 반환하지 않을지 결정이 필요합니다.
💡 단일 값과 배열을 동일하게 처리하는지 테스트하세요. category: 'electronics'와 category: ['electronics']가 같은 결과를 내야 일관성이 있습니다.
💡 배열의 순서가 결과에 영향을 주는지 확인하세요. 대부분의 OR 필터는 순서 무관이지만, 우선순위 정렬 같은 경우는 순서가 중요할 수 있습니다.
💡 배열 크기 제한을 테스트하세요. 100개의 카테고리를 선택하면 서버가 과부하될 수 있으므로 적절한 제한과 에러 메시지가 필요합니다.
💡 중복값이 들어왔을 때 자동으로 제거되는지 확인하세요. category: ['books', 'books', 'books']를 ['books']로 처리하는 것이 효율적입니다.
5. 선택적 쿼리 파라미터 테스트 - 기본값과 유연성
시작하며
여러분이 게시글 목록 API를 만들었는데, 페이지 번호나 정렬 옵션 같은 파라미터를 전달하지 않았을 때 어떻게 동작해야 할지 고민해본 적 있나요? 쿼리 파라미터를 필수로 만들면 클라이언트가 매번 모든 값을 전달해야 해서 불편하고, 선택적으로 만들면 기본값 처리를 제대로 테스트하지 않으면 예상치 못한 동작이 발생할 수 있습니다.
이런 문제는 API 설계에서 매우 중요한 부분입니다. 사용자 경험 측면에서 선택적 파라미터는 필수적이지만, 각 파라미터가 없을 때의 기본 동작을 명확히 정의하고 테스트하지 않으면 클라이언트와 서버 간 혼란이 발생합니다.
예를 들어 정렬 옵션을 생략했을 때 날짜순인지 인기순인지 모호하면 사용자는 혼란스러워합니다. 바로 이럴 때 필요한 것이 선택적 쿼리 파라미터 테스트입니다.
모든 파라미터 조합(전부 있을 때, 일부만 있을 때, 전부 없을 때)에 대해 API가 일관되고 예측 가능한 동작을 하는지 검증할 수 있습니다.
개요
간단히 말해서, 선택적 쿼리 파라미터 테스트는 특정 파라미터가 제공되지 않았을 때 API가 적절한 기본값을 사용하고, 제공되었을 때는 그 값을 올바르게 적용하는지 확인하는 작업입니다. 실무에서는 거의 모든 GET 요청에서 사용됩니다.
예를 들어, 블로그 플랫폼에서 게시글 목록을 조회할 때 page, limit, sortBy, order 같은 파라미터는 모두 선택적이지만 각각 의미 있는 기본값을 가져야 합니다. 사용자가 아무 옵션 없이 /posts를 호출해도 첫 페이지의 최신 글 20개가 보이는 것이 좋은 UX입니다.
기존에는 모든 파라미터를 항상 전달하는 테스트만 작성했다면, 이제는 파라미터를 생략한 경우, 일부만 제공한 경우, 잘못된 값을 제공한 경우 등 다양한 시나리오를 체계적으로 테스트할 수 있습니다. 특히 기본값이 문서화되고 테스트로 보장되면 API의 사용성이 크게 향상됩니다.
선택적 쿼리 파라미터의 핵심 특징은 첫째, 각 파라미터의 기본값이 명확하고 일관되어야 하며, 둘째, 파라미터 조합이 달라져도 예측 가능한 동작을 해야 하고, 셋째, 잘못된 값이 들어왔을 때 기본값으로 대체할지 에러를 반환할지 명확한 정책이 있어야 합니다. 이러한 특징들을 테스트하면 API 문서 없이도 직관적으로 사용할 수 있는 API가 됩니다.
코드 예제
// 게시글 목록 조회 - 선택적 쿼리 파라미터와 기본값 테스트
const request = require('supertest');
const app = require('../app');
describe('GET /posts - 선택적 쿼리 파라미터 테스트', () => {
it('모든 쿼리 파라미터 없이 호출시 기본값을 사용해야 함', async () => {
// when: 쿼리 파라미터 없이 요청
const response = await request(app)
.get('/posts') // 쿼리 없음
.expect(200);
// then: 기본값(페이지 1, 20개 제한, 날짜 내림차순)이 적용되었는지 검증
expect(response.body.posts).toBeInstanceOf(Array);
expect(response.body.posts.length).toBeLessThanOrEqual(20); // 기본 limit
expect(response.body.page).toBe(1); // 기본 page
expect(response.body.totalPages).toBeDefined();
});
it('일부 쿼리만 제공하면 나머지는 기본값을 사용해야 함', async () => {
// when: limit만 제공하고 page는 생략
const response = await request(app)
.get('/posts')
.query({ limit: 5 }) // page는 기본값 1 사용
.expect(200);
// then: limit은 적용되고 page는 기본값
expect(response.body.posts.length).toBeLessThanOrEqual(5);
expect(response.body.page).toBe(1);
});
it('잘못된 값은 기본값으로 대체하거나 에러를 반환해야 함', async () => {
// when: 음수 page 제공
const response = await request(app)
.get('/posts')
.query({ page: -1, limit: 10 });
// then: 400 에러 또는 기본값 사용 (정책에 따라)
if (response.status === 400) {
expect(response.body).toHaveProperty('error');
} else {
expect(response.status).toBe(200);
expect(response.body.page).toBe(1); // 음수는 1로 대체
}
});
});
설명
이것이 하는 일: 위 테스트 코드는 게시글 목록 API가 쿼리 파라미터를 생략했을 때 적절한 기본값을 사용하고, 일부만 제공했을 때도 일관되게 동작하며, 잘못된 값에 대해서는 명확한 정책을 따르는지 검증합니다. 첫 번째 테스트에서는 .get('/posts')만 호출하고 .query()를 아예 사용하지 않습니다.
이는 사용자가 가장 간단하게 API를 호출하는 방식으로, 이때 기본값이 어떻게 적용되는지 확인하는 것이 매우 중요합니다. 응답에서 posts 배열의 크기가 20 이하인지, page가 1인지 확인하여 기본값이 제대로 적용되었음을 검증합니다.
두 번째 테스트에서는 limit만 5로 지정하고 page는 생략합니다. 이런 부분 적용 시나리오가 실무에서 매우 흔한데, 사용자가 "처음 5개만 보고 싶다"라고 할 때 페이지 번호까지 강제하는 것은 불편하기 때문입니다.
응답에서 limit은 지정한 대로 적용되고 page는 기본값 1이 사용되는지 확인합니다. 세 번째 테스트에서는 잘못된 값(음수 페이지)을 전달했을 때의 동작을 검증합니다.
이 부분이 흥미로운데, API 정책에 따라 두 가지 접근이 가능합니다. 엄격한 검증을 선호하면 400 Bad Request를 반환하고, 사용자 친화적인 접근을 선호하면 기본값으로 자동 대체합니다.
테스트에서는 두 경우를 모두 처리하여 어떤 정책을 선택하든 일관되게 동작하는지 확인합니다. 여러분이 이 패턴을 사용하면 API를 처음 사용하는 개발자도 쉽게 시작할 수 있습니다.
최소한의 파라미터로 호출해서 기본 동작을 확인한 후, 점진적으로 옵션을 추가하는 학습 곡선이 완만해집니다. 또한 프론트엔드에서 조건부로 파라미터를 추가하는 로직을 작성할 때, 파라미터가 undefined여도 백엔드가 안전하게 처리한다는 확신을 가질 수 있어서 클라이언트 코드가 간결해집니다.
실전 팁
💡 기본값을 상수로 정의하고 테스트와 실제 코드에서 동일한 상수를 참조하세요. 기본값이 변경될 때 한 곳만 수정하면 됩니다.
💡 페이지네이션의 최대 limit를 테스트하세요. 사용자가 limit=999999를 요청하면 서버 부하를 방지하기 위해 제한해야 합니다.
💡 쿼리 파라미터의 타입 변환을 테스트하세요. URL에서는 모든 값이 문자열이므로 '10'을 숫자 10으로 변환하는 로직이 필요합니다.
💡 boolean 타입 파라미터(isPublished=true)는 'true', '1', 'yes' 등 다양한 표현을 허용하는지 테스트하세요.
💡 기본값을 API 응답에 포함시켜서 클라이언트가 어떤 값이 적용되었는지 알 수 있게 하세요. { page: 1, limit: 20, posts: [...] } 형태로요.
6. 잘못된 파라미터 검증 테스트 - 에러 처리의 중요성
시작하며
여러분이 API를 개발했는데, 사용자가 잘못된 타입의 값을 전달하거나 허용되지 않는 값을 보냈을 때 서버가 크래시되거나 이상한 데이터를 반환한 경험이 있나요? 예를 들어 숫자를 기대하는 age 파라미터에 문자열 "abc"를 보내거나, 음수가 들어올 수 없는 price에 -1000을 보내는 경우입니다.
이런 문제는 프로덕션 환경에서 치명적입니다. 잘못된 입력으로 인한 서버 크래시는 서비스 전체를 마비시킬 수 있고, 부적절한 에러 메시지는 클라이언트 개발자를 혼란스럽게 만들며, 보안 측면에서도 입력 검증 부족은 SQL 인젝션이나 다른 공격의 통로가 될 수 있습니다.
바로 이럴 때 필요한 것이 잘못된 파라미터 검증 테스트입니다. 정상적인 경로만큼이나 비정상적인 경로도 철저하게 테스트하여, API가 어떤 입력에도 예측 가능하고 안전하게 동작하는지 보장할 수 있습니다.
개요
간단히 말해서, 잘못된 파라미터 검증 테스트는 타입 오류, 범위 초과, 형식 불일치, 필수 파라미터 누락 등 다양한 비정상 입력에 대해 API가 적절한 HTTP 상태 코드(주로 400)와 명확한 에러 메시지를 반환하는지 확인하는 작업입니다. 실무에서는 입력 검증이 보안과 안정성의 첫 번째 방어선입니다.
예를 들어, 금융 앱에서 송금액에 음수가 들어오면 안 되고, SNS에서 게시글 길이가 제한을 초과하면 안 되며, 날짜 형식이 잘못되면 데이터베이스 에러가 발생할 수 있습니다. 이 모든 케이스를 사전에 검증하고 친절한 에러 메시지를 반환해야 클라이언트가 적절히 대응할 수 있습니다.
기존에는 성공 케이스만 테스트하는 경향이 있었다면, 이제는 실패 케이스를 체계적으로 분류하고 테스트할 수 있습니다. 타입 에러, 범위 에러, 형식 에러, 필수값 누락 에러를 각각 다른 테스트 케이스로 만들면 어떤 검증이 빠졌는지 명확히 알 수 있습니다.
잘못된 파라미터 검증의 핵심 특징은 첫째, 400 Bad Request 상태 코드를 일관되게 사용해야 하고, 둘째, 에러 메시지에 어떤 파라미터가 잘못되었고 왜 잘못되었는지 구체적으로 명시해야 하며, 셋째, 여러 파라미터가 동시에 잘못되었을 때도 모든 에러를 알려주는 것이 좋습니다. 이를 통해 클라이언트 개발자는 한 번의 요청으로 모든 문제를 파악할 수 있습니다.
코드 예제
// 파라미터 검증 에러 테스트 - 다양한 잘못된 입력 처리
const request = require('supertest');
const app = require('../app');
describe('GET /products - 파라미터 검증 테스트', () => {
it('숫자여야 하는 파라미터에 문자열을 보내면 400 에러', async () => {
// when: minPrice에 문자열 전달
const response = await request(app)
.get('/products')
.query({ minPrice: 'not-a-number' })
.expect(400);
// then: 명확한 에러 메시지 검증
expect(response.body).toHaveProperty('error');
expect(response.body.error).toContain('minPrice');
expect(response.body.error).toContain('number');
});
it('음수가 허용되지 않는 파라미터에 음수를 보내면 400 에러', async () => {
// when: page에 음수 전달
const response = await request(app)
.get('/products')
.query({ page: -5 })
.expect(400);
// then: 범위 에러 메시지 검증
expect(response.body.error).toContain('page');
expect(response.body.error).toMatch(/positive|greater than 0/i);
});
it('잘못된 enum 값을 보내면 400 에러', async () => {
// when: sortBy에 허용되지 않는 값 전달
const response = await request(app)
.get('/products')
.query({ sortBy: 'invalid-field' })
.expect(400);
// then: 허용되는 값 목록이 에러 메시지에 포함되는지 검증
expect(response.body.error).toContain('sortBy');
expect(response.body.error).toMatch(/price|name|date/i); // 허용되는 값들
});
it('여러 파라미터가 동시에 잘못되면 모든 에러를 반환해야 함', async () => {
// when: 여러 파라미터가 동시에 잘못됨
const response = await request(app)
.get('/products')
.query({ page: -1, limit: 'abc', sortBy: 'invalid' })
.expect(400);
// then: 모든 에러가 포함되어야 함 (선택적 구현)
expect(response.body.errors).toBeInstanceOf(Array);
expect(response.body.errors.length).toBeGreaterThanOrEqual(3);
});
});
설명
이것이 하는 일: 위 테스트 코드는 상품 검색 API가 다양한 유형의 잘못된 입력을 적절하게 거부하고, 클라이언트가 문제를 이해하고 수정할 수 있도록 명확한 에러 메시지를 제공하는지 검증합니다. 첫 번째 테스트는 타입 에러를 다룹니다.
minPrice는 숫자여야 하는데 문자열 'not-a-number'를 보냈을 때, API가 400 상태 코드를 반환하고 에러 메시지에 어떤 필드(minPrice)가 어떤 타입(number)이어야 하는지 명시하는지 확인합니다. 이런 구체적인 에러 메시지가 없으면 클라이언트 개발자가 디버깅에 많은 시간을 낭비합니다.
두 번째 테스트는 범위 에러를 다룹니다. 페이지 번호는 양수여야 하는데 -5를 보냈을 때, 에러 메시지에 'positive' 또는 'greater than 0' 같은 힌트가 포함되는지 검증합니다.
.toMatch()를 사용해서 정규식으로 매칭하면 에러 메시지의 정확한 문구가 변경되어도 테스트가 깨지지 않습니다. 세 번째 테스트는 enum 검증을 다룹니다.
sortBy는 'price', 'name', 'date' 중 하나여야 하는데 'invalid-field'를 보냈을 때, 에러 메시지에 허용되는 값 목록이 포함되는지 확인합니다. 이는 매우 중요한데, 사용자가 어떤 값을 사용할 수 있는지 API 문서를 보지 않고도 에러 메시지에서 바로 알 수 있기 때문입니다.
네 번째 테스트는 여러 파라미터가 동시에 잘못되었을 때를 다룹니다. 이상적으로는 첫 번째 에러만 반환하지 않고 모든 검증 에러를 배열로 반환해야 클라이언트가 한 번에 모든 문제를 파악하고 수정할 수 있습니다.
errors 배열에 각 필드별 에러 객체가 들어있는 구조가 좋은 패턴입니다. 여러분이 이런 테스트를 작성하면 API의 견고성이 크게 향상됩니다.
예상치 못한 입력으로 인한 서버 크래시를 방지하고, 클라이언트 개발자에게 명확한 피드백을 제공하며, 보안 취약점을 사전에 차단할 수 있습니다. 또한 이런 테스트는 API 문서의 역할도 하는데, 테스트 코드를 보면 어떤 파라미터가 어떤 제약사항을 가지는지 명확히 알 수 있습니다.
실전 팁
💡 에러 응답 형식을 표준화하세요. { error: string } 또는 { errors: [{ field: string, message: string }] } 같은 일관된 구조를 사용하세요.
💡 joi, yup, zod 같은 검증 라이브러리를 사용하면 복잡한 검증 로직을 선언적으로 작성하고 테스트할 수 있습니다.
💡 경계값 테스트를 잊지 마세요. 0, -1, 최대값+1, 빈 문자열, null, undefined 등을 모두 테스트해야 합니다.
💡 에러 메시지에 민감한 정보(데이터베이스 구조, 내부 경로 등)가 노출되지 않도록 주의하세요. 보안 취약점이 될 수 있습니다.
💡 국제화를 고려한다면 에러 메시지에 에러 코드(ERR_INVALID_PRICE)를 함께 반환하여 클라이언트가 다국어 메시지로 변환할 수 있게 하세요.
7. 중첩된 경로 파라미터 테스트 - 계층적 리소스 관리
시작하며
여러분이 블로그 플랫폼을 개발하면서 "특정 게시글의 특정 댓글"이나 "특정 카테고리의 특정 상품"처럼 계층적인 리소스 구조를 구현한 적 있나요? 예를 들어 /posts/123/comments/456 같은 URL에서 게시글 ID(123)와 댓글 ID(456)를 모두 처리해야 하는 경우, 두 ID의 관계를 제대로 검증하지 않으면 다른 게시글의 댓글을 조회하는 보안 문제가 발생할 수 있습니다.
이런 문제는 RESTful API 설계에서 매우 흔하면서도 까다롭습니다. 단순히 두 개의 파라미터를 받는 것이 아니라, 첫 번째 리소스가 존재하는지, 두 번째 리소스가 첫 번째 리소스에 속하는지를 모두 검증해야 합니다.
많은 개발자가 자식 리소스만 확인하고 부모와의 관계는 검증하지 않아서 권한 우회 버그가 발생합니다. 바로 이럴 때 필요한 것이 중첩된 경로 파라미터 테스트입니다.
계층적 관계가 올바르게 유지되는지, 잘못된 조합에 대해서는 404나 403 에러를 반환하는지 철저하게 검증할 수 있습니다.
개요
간단히 말해서, 중첩된 경로 파라미터 테스트는 URL 경로에 여러 단계의 리소스 식별자가 포함되었을 때, 각 리소스의 존재와 상호 관계를 모두 검증하는 작업입니다. 실무에서는 계층적 데이터 구조가 매우 흔합니다.
예를 들어, 전자상거래에서 "카테고리 > 하위카테고리 > 상품", SNS에서 "사용자 > 게시글 > 댓글 > 대댓글", 프로젝트 관리에서 "조직 > 프로젝트 > 작업 > 하위작업" 같은 구조가 모두 중첩된 경로로 표현됩니다. 각 단계마다 권한과 소유권을 검증해야 하므로 테스트가 매우 중요합니다.
기존에는 자식 리소스가 존재하는지만 확인했다면, 이제는 "자식이 정말 해당 부모에 속하는가?"라는 관계 검증까지 테스트할 수 있습니다. 예를 들어 게시글 123의 댓글 456을 조회할 때, 댓글 456이 실제로 게시글 123에 달린 것인지 확인해야 합니다.
중첩된 경로 파라미터의 핵심 특징은 첫째, 상위 리소스가 존재하지 않으면 하위 리소스를 찾지 않고 바로 404를 반환해야 하고, 둘째, 두 리소스 모두 존재하지만 관계가 없으면 404 또는 403을 반환해야 하며, 셋째, URL 패턴이 깊어질수록 각 단계의 검증이 누적되어야 합니다. 이를 통해 안전하고 직관적인 API를 만들 수 있습니다.
코드 예제
// 중첩된 경로 파라미터 테스트 - 게시글의 특정 댓글 조회
const request = require('supertest');
const app = require('../app');
describe('GET /posts/:postId/comments/:commentId - 중첩 경로 테스트', () => {
it('유효한 게시글과 댓글 ID로 특정 댓글을 조회해야 함', async () => {
// given: 유효한 게시글 ID와 해당 게시글에 속한 댓글 ID
const postId = 100;
const commentId = 200;
// when: 중첩된 경로로 요청
const response = await request(app)
.get(`/posts/${postId}/comments/${commentId}`)
.expect(200);
// then: 댓글이 올바른 게시글에 속하는지 검증
expect(response.body).toHaveProperty('id', commentId);
expect(response.body).toHaveProperty('postId', postId);
expect(response.body).toHaveProperty('content');
});
it('존재하지 않는 게시글 ID는 404를 반환해야 함', async () => {
// when: 존재하지 않는 postId
await request(app)
.get('/posts/99999/comments/200')
.expect(404);
});
it('게시글은 존재하지만 댓글이 해당 게시글에 속하지 않으면 404', async () => {
// given: 게시글 100은 존재하지만 댓글 999는 게시글 200에 속함
const postId = 100;
const wrongCommentId = 999; // 다른 게시글의 댓글
// when: 잘못된 조합으로 요청
const response = await request(app)
.get(`/posts/${postId}/comments/${wrongCommentId}`)
.expect(404);
// then: 관계 에러 메시지 (선택적)
expect(response.body.error).toMatch(/not found|does not belong/i);
});
});
설명
이것이 하는 일: 위 테스트 코드는 특정 게시글의 특정 댓글을 조회하는 API가 두 리소스의 존재와 상호 관계를 올바르게 검증하고, 잘못된 조합에 대해서는 적절한 에러를 반환하는지 확인합니다. 첫 번째 테스트는 정상 케이스를 다룹니다.
postId와 commentId를 모두 템플릿 리터럴로 URL에 삽입하고, 응답에서 댓글의 id가 요청한 commentId와 일치하고, 더 중요하게는 댓글의 postId 필드가 요청한 게시글 ID와 일치하는지 확인합니다. 이 postId 검증이 핵심인데, 이를 통해 댓글이 정말 해당 게시글에 속한다는 것을 보장합니다.
두 번째 테스트는 부모 리소스가 존재하지 않는 경우를 다룹니다. 게시글 99999가 없으면, 그 게시글의 댓글을 찾으려는 시도 자체가 무의미하므로 404를 반환해야 합니다.
이는 계층적 검증의 기본 원칙으로, 상위 단계부터 순차적으로 검증하는 것이 효율적이고 명확합니다. 세 번째 테스트는 가장 중요하면서도 자주 누락되는 케이스입니다.
게시글 100도 존재하고 댓글 999도 존재하지만, 댓글 999가 게시글 100이 아닌 다른 게시글(예: 게시글 200)에 달려있는 경우입니다. 이 경우 404를 반환해야 하는데, 많은 API가 이를 검증하지 않아서 다른 사람의 댓글을 수정하거나 삭제할 수 있는 보안 취약점이 발생합니다.
네 번째로, 에러 메시지도 검증합니다. "Comment not found" 또는 "Comment does not belong to this post" 같은 명확한 메시지를 제공하면 클라이언트 개발자가 문제를 빠르게 파악할 수 있습니다.
정규식 /not found|does not belong/i를 사용하면 메시지 문구가 약간 달라져도 테스트가 통과합니다. 여러분이 이 패턴을 사용하면 권한 우회 공격을 효과적으로 방어할 수 있습니다.
특히 IDOR(Insecure Direct Object Reference) 취약점, 즉 다른 사용자의 리소스를 ID만 바꿔서 접근하는 공격을 차단할 수 있습니다. 또한 3단계 이상의 중첩(/orgs/:orgId/projects/:projectId/tasks/:taskId)에도 같은 원칙을 적용하여 체계적으로 테스트할 수 있습니다.
실전 팁
💡 관계 검증을 데이터베이스 쿼리에서 처리하세요. JOIN이나 WHERE 절로 부모-자식 관계를 한 번에 확인하면 성능도 좋고 안전합니다.
💡 3단계 이상 중첩될 때는 미들웨어로 각 단계의 검증을 분리하세요. 코드 재사용성이 높아지고 테스트도 쉬워집니다.
💡 부모 리소스 정보를 응답에 포함시키면 클라이언트가 breadcrumb UI를 만들기 쉽습니다. { comment: {...}, post: { id, title } }처럼요.
💡 DELETE나 PUT 요청에서는 관계 검증이 더욱 중요합니다. 다른 사람의 댓글을 삭제하는 것을 방지해야 합니다.
💡 자식 리소스를 생성할 때(POST /posts/:postId/comments)도 부모 존재 여부를 반드시 확인하세요. 존재하지 않는 게시글에 댓글을 달면 안 됩니다.
8. PUT DELETE 요청의 파라미터 테스트 - 수정과 삭제 검증
시작하며
여러분이 사용자 정보 수정 API를 만들었는데, 경로 파라미터로 받은 사용자 ID와 요청 본문(body)의 사용자 ID가 일치하는지 확인하지 않아서 다른 사용자의 정보를 수정할 수 있는 버그가 발생한 적 있나요? 또는 삭제 API에서 이미 삭제된 리소스를 다시 삭제하려 할 때 어떤 상태 코드를 반환해야 할지 고민해본 적 있으신가요?
이런 문제는 GET 요청보다 PUT, PATCH, DELETE 같은 변경 작업에서 더욱 치명적입니다. 데이터를 읽기만 하는 GET 요청의 버그는 잘못된 정보를 보여주는 수준이지만, 변경 작업의 버그는 데이터 손실, 권한 우회, 비즈니스 로직 오류 등 심각한 결과를 초래할 수 있습니다.
바로 이럴 때 필요한 것이 PUT/DELETE 요청의 파라미터 테스트입니다. 경로 파라미터, 요청 본문, 권한 정보가 모두 일치하는지 검증하고, 멱등성(idempotency) 같은 HTTP 표준도 준수하는지 확인할 수 있습니다.
개요
간단히 말해서, PUT/DELETE 요청의 파라미터 테스트는 리소스를 수정하거나 삭제하는 API가 경로 파라미터로 대상을 올바르게 식별하고, 요청 본문의 데이터가 일관되며, 권한 검증이 적절히 이루어지는지 확인하는 작업입니다. 실무에서는 변경 작업의 안전성이 매우 중요합니다.
예를 들어, 쇼핑몰에서 주문 상태를 '배송 중'으로 변경할 때 경로의 주문 ID와 본문의 주문 ID가 일치하는지, 사용자가 해당 주문의 소유자인지, 이미 배송 완료 상태가 아닌지 등을 모두 검증해야 합니다. 하나라도 빠지면 비즈니스 로직이 무너집니다.
기존에는 정상적인 수정/삭제만 테스트했다면, 이제는 권한 없는 사용자의 시도, 존재하지 않는 리소스, 이미 삭제된 리소스, ID 불일치 등 다양한 실패 시나리오를 체계적으로 테스트할 수 있습니다. 특히 DELETE의 멱등성(같은 요청을 여러 번 해도 결과가 같음)을 보장하는 것이 중요합니다.
PUT/DELETE 요청 테스트의 핵심 특징은 첫째, 경로 파라미터와 요청 본문의 ID가 있다면 반드시 일치해야 하고, 둘째, 성공 시 PUT은 200, DELETE는 204(No Content)를 반환하는 것이 표준이며, 셋째, 실패 시 404(리소스 없음)와 403(권한 없음)을 명확히 구분해야 합니다. 이를 통해 RESTful 표준을 준수하는 안전한 API를 만들 수 있습니다.
코드 예제
// PUT/DELETE 요청 테스트 - 사용자 정보 수정 및 삭제
const request = require('supertest');
const app = require('../app');
describe('PUT /users/:id - 사용자 수정 테스트', () => {
it('유효한 ID로 사용자 정보를 수정해야 함', async () => {
// given: 수정할 사용자 ID와 새 데이터
const userId = 10;
const updateData = { name: 'Updated Name', email: 'new@example.com' };
// when: PUT 요청으로 수정
const response = await request(app)
.put(`/users/${userId}`)
.send(updateData) // 요청 본문에 수정할 데이터
.expect(200);
// then: 응답에 수정된 데이터가 포함되는지 검증
expect(response.body.id).toBe(userId);
expect(response.body.name).toBe(updateData.name);
expect(response.body.email).toBe(updateData.email);
});
it('경로 ID와 본문 ID가 불일치하면 400 에러', async () => {
// when: 경로와 본문의 ID가 다름
const response = await request(app)
.put('/users/10')
.send({ id: 20, name: 'Test' }) // 10과 20이 불일치
.expect(400);
// then: ID 불일치 에러
expect(response.body.error).toMatch(/id.*mismatch|inconsistent/i);
});
});
describe('DELETE /users/:id - 사용자 삭제 테스트', () => {
it('유효한 ID로 사용자를 삭제해야 함', async () => {
// given: 삭제할 사용자 ID
const userId = 15;
// when: DELETE 요청
await request(app)
.delete(`/users/${userId}`)
.expect(204); // 삭제 성공 시 204 No Content
// then: 다시 조회하면 404 (선택적 검증)
await request(app)
.get(`/users/${userId}`)
.expect(404);
});
it('이미 삭제된 리소스를 다시 삭제하면 404', async () => {
// when: 존재하지 않는 사용자 삭제 시도
await request(app)
.delete('/users/99999')
.expect(404);
});
});
설명
이것이 하는 일: 위 테스트 코드는 사용자 수정 및 삭제 API가 경로 파라미터로 대상을 식별하고, 요청 본문의 일관성을 검증하며, 적절한 HTTP 상태 코드를 반환하는지 확인합니다. 첫 번째 PUT 테스트에서는 .put() 메서드로 경로를 지정하고 .send()로 요청 본문을 전달합니다.
GET 요청의 .query()와 달리 PUT/POST는 .send()를 사용한다는 점이 중요합니다. 응답에서는 수정된 데이터가 올바르게 반영되었는지 확인하고, 특히 응답의 id가 요청한 userId와 일치하는지 검증하여 엉뚱한 사용자를 수정하지 않았음을 보장합니다.
두 번째 PUT 테스트는 매우 중요한 보안 검증입니다. 경로에는 /users/10으로 사용자 10을 지정했는데, 요청 본문에 `{ id: 20, ...
}`처럼 다른 ID가 들어있으면 혼란스러울 뿐 아니라 보안 취약점이 될 수 있습니다. 이런 불일치를 400 Bad Request로 거부하는 것이 안전한 설계입니다.
에러 메시지에 'mismatch'나 'inconsistent' 같은 키워드가 포함되는지 확인합니다. 세 번째 DELETE 테스트에서는 .expect(204)로 삭제 성공을 검증합니다.
204 No Content는 HTTP 표준에서 "성공했지만 반환할 본문이 없음"을 의미하는데, DELETE가 완료되면 해당 리소스가 없어졌으므로 본문을 반환할 이유가 없습니다. 추가로, 삭제 후 GET 요청으로 정말 삭제되었는지 확인하는 검증도 포함할 수 있습니다.
네 번째 DELETE 테스트는 멱등성을 다룹니다. 이미 삭제되었거나 애초에 존재하지 않는 리소스를 삭제하려 하면 404를 반환하는 것이 일반적입니다.
일부 API는 멱등성을 위해 이미 없어도 204를 반환하기도 하는데, 어떤 방식이든 일관되게 테스트하면 됩니다. 중요한 것은 서버가 크래시하지 않고 명확한 응답을 주는 것입니다.
여러분이 이런 테스트를 작성하면 데이터 변경 작업의 안전성이 크게 향상됩니다. 특히 다중 사용자 환경에서 동시성 문제, 권한 우회, 데이터 무결성 위반 등을 사전에 방지할 수 있습니다.
또한 HTTP 표준을 준수하면 클라이언트 라이브러리나 프록시 서버가 예상대로 동작하여 시스템 전체의 신뢰성이 올라갑니다.
실전 팁
💡 PATCH와 PUT을 구분하세요. PUT은 전체 교체, PATCH는 부분 수정입니다. 테스트에서도 PUT은 모든 필드를, PATCH는 일부 필드만 검증하세요.
💡 낙관적 잠금(Optimistic Locking)을 사용한다면 버전 번호나 ETag를 파라미터로 테스트하세요. 동시 수정 충돌을 감지할 수 있습니다.
💡 DELETE 후 관련 리소스도 함께 삭제되는지(cascade) 테스트하세요. 게시글 삭제 시 댓글도 삭제되어야 한다면 이를 검증하세요.
💡 soft delete(논리 삭제)를 사용한다면 DELETE 후에도 데이터가 DB에 남아있되 deleted_at 필드가 설정되는지 확인하세요.
💡 수정/삭제 후 변경 이력(audit log)이 생성되는지 테스트하면 규정 준수와 디버깅에 도움이 됩니다.