Supertest를 사용한 API 테스트에 중점을 둔 통합 테스트 종합 가이드. 설정, 모범 사례, 고급 기술을 다루어 견고한 애플리케이션 테스트를 보장합니다.
통합 테스트: Supertest로 API 테스트 마스터하기
소프트웨어 개발 영역에서 개별 구성 요소가 독립적으로 올바르게 작동하는지 확인하는 것(단위 테스트)은 매우 중요합니다. 하지만 이러한 구성 요소들이 서로 원활하게 함께 작동하는지 검증하는 것 또한 똑같이 중요합니다. 바로 이 지점에서 통합 테스트가 필요합니다. 통합 테스트는 애플리케이션 내의 여러 모듈 또는 서비스 간의 상호 작용을 검증하는 데 중점을 둡니다. 이 글에서는 통합 테스트, 특히 Node.js에서 HTTP 단언을 테스트하기 위한 강력하고 사용자 친화적인 라이브러리인 Supertest를 사용한 API 테스트에 대해 심도 있게 다룹니다.
통합 테스트란 무엇인가?
통합 테스트는 개별 소프트웨어 모듈을 결합하여 그룹으로 테스트하는 소프트웨어 테스트 유형입니다. 통합된 단위들 간의 상호 작용에서 발생하는 결함을 찾아내는 것을 목표로 합니다. 개별 구성 요소에 초점을 맞추는 단위 테스트와 달리, 통합 테스트는 모듈 간의 데이터 흐름과 제어 흐름을 검증합니다. 일반적인 통합 테스트 접근 방식은 다음과 같습니다:
- 하향식 통합: 최상위 수준의 모듈부터 시작하여 아래로 통합하는 방식입니다.
- 상향식 통합: 최하위 수준의 모듈부터 시작하여 위로 통합하는 방식입니다.
- 빅뱅 통합: 모든 모듈을 동시에 통합하는 방식입니다. 이 접근 방식은 문제 분리가 어려워 일반적으로 덜 권장됩니다.
- 샌드위치 통합: 하향식 통합과 상향식 통합의 조합입니다.
API의 맥락에서 통합 테스트는 여러 API가 함께 올바르게 작동하는지, 그들 사이에 전달되는 데이터가 일관성이 있는지, 그리고 전체 시스템이 예상대로 작동하는지 검증하는 것을 포함합니다. 예를 들어, 상품 관리, 사용자 인증, 결제 처리를 위한 별도의 API를 가진 전자 상거래 애플리케이션을 상상해 보십시오. 통합 테스트는 이러한 API들이 올바르게 통신하여 사용자가 상품을 탐색하고, 안전하게 로그인하며, 구매를 완료할 수 있도록 보장합니다.
API 통합 테스트가 중요한 이유는 무엇인가?
API 통합 테스트는 여러 가지 이유로 매우 중요합니다:
- 시스템 신뢰성 보장: 개발 주기 초기에 통합 문제를 식별하여 프로덕션 환경에서의 예기치 않은 장애를 예방하는 데 도움이 됩니다.
- 데이터 무결성 검증: 여러 API 간에 데이터가 올바르게 전송되고 변환되는지 확인합니다.
- 애플리케이션 성능 향상: API 상호 작용과 관련된 성능 병목 현상을 발견할 수 있습니다.
- 보안 강화: 부적절한 API 통합으로 인해 발생하는 보안 취약점을 식별할 수 있습니다. 예를 들어, API가 통신할 때 적절한 인증 및 권한 부여를 보장합니다.
- 개발 비용 절감: 통합 문제를 조기에 수정하는 것이 개발 수명 주기 후반에 해결하는 것보다 훨씬 저렴합니다.
글로벌 여행 예약 플랫폼을 생각해 봅시다. API 통합 테스트는 다양한 국가의 항공편 예약, 호텔 예약 및 결제 게이트웨이를 처리하는 API 간의 원활한 통신을 보장하는 데 가장 중요합니다. 이러한 API를 제대로 통합하지 못하면 잘못된 예약, 결제 실패, 좋지 않은 사용자 경험으로 이어져 플랫폼의 평판과 수익에 부정적인 영향을 미칠 수 있습니다.
Supertest 소개: API 테스트를 위한 강력한 도구
Supertest는 HTTP 요청을 테스트하기 위한 고수준 추상화 라이브러리입니다. 애플리케이션에 요청을 보내고 응답에 대한 단언을 수행하는 편리하고 유창한(fluent) API를 제공합니다. Node.js를 기반으로 구축된 Supertest는 특히 Node.js HTTP 서버 테스트를 위해 설계되었습니다. Jest나 Mocha와 같은 인기 있는 테스트 프레임워크와 매우 잘 작동합니다.
Supertest의 주요 특징:
- 사용 용이성: Supertest는 HTTP 요청을 보내고 단언을 작성하기 위한 간단하고 직관적인 API를 제공합니다.
- 비동기 테스트: 비동기 작업을 원활하게 처리하므로 비동기 로직에 의존하는 API를 테스트하는 데 이상적입니다.
- 유창한 인터페이스: 유창한(fluent) 인터페이스를 제공하여 간결하고 가독성 높은 테스트를 위해 메서드를 체이닝할 수 있습니다.
- 포괄적인 단언 지원: 응답 상태 코드, 헤더 및 본문을 검증하기 위한 광범위한 단언을 지원합니다.
- 테스트 프레임워크와의 통합: Jest, Mocha와 같은 인기 있는 테스트 프레임워크와 원활하게 통합되어 기존 테스트 인프라를 사용할 수 있습니다.
테스트 환경 설정하기
시작하기 전에 기본적인 테스트 환경을 설정해 보겠습니다. Node.js와 npm(또는 yarn)이 설치되어 있다고 가정합니다. 테스트 프레임워크로는 Jest를, API 테스트에는 Supertest를 사용하겠습니다.
- Node.js 프로젝트 생성하기:
mkdir api-testing-example
cd api-testing-example
npm init -y
- 의존성 설치하기:
npm install --save-dev jest supertest
npm install express # 또는 API 생성을 위해 선호하는 프레임워크
- Jest 설정하기:
package.json
파일에 다음을 추가하세요:
{
"scripts": {
"test": "jest"
}
}
- 간단한 API 엔드포인트 생성하기:
app.js
(또는 유사한 이름)라는 파일을 만들고 다음 코드를 추가하세요:
const express = require('express');
const app = express();
const port = 3000;
app.get('/hello', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
module.exports = app; // 테스트를 위해 내보내기
첫 Supertest 테스트 작성하기
이제 환경 설정이 완료되었으니, API 엔드포인트를 검증하기 위한 간단한 Supertest 테스트를 작성해 보겠습니다. 프로젝트의 루트에 app.test.js
(또는 유사한 이름) 파일을 생성하세요:
const request = require('supertest');
const app = require('./app');
describe('GET /hello', () => {
it('responds with 200 OK and returns "Hello, World!"', async () => {
const response = await request(app).get('/hello');
expect(response.statusCode).toBe(200);
expect(response.text).toBe('Hello, World!');
});
});
설명:
supertest
와 Express 앱을 가져옵니다.describe
를 사용하여 테스트를 그룹화합니다.it
을 사용하여 특정 테스트 케이스를 정의합니다.request(app)
를 사용하여 우리 앱에 요청을 보낼 Supertest 에이전트를 생성합니다..get('/hello')
를 사용하여/hello
엔드포인트로 GET 요청을 보냅니다.await
를 사용하여 응답을 기다립니다. Supertest의 메서드는 프로미스(promise)를 반환하므로, 더 깔끔한 코드를 위해 async/await를 사용할 수 있습니다.expect(response.statusCode).toBe(200)
을 사용하여 응답 상태 코드가 200 OK인지 단언합니다.expect(response.text).toBe('Hello, World!')
를 사용하여 응답 본문이 "Hello, World!"인지 단언합니다.
테스트를 실행하려면 터미널에서 다음 명령을 실행하세요:
npm test
모든 것이 올바르게 설정되었다면 테스트가 통과하는 것을 볼 수 있습니다.
고급 Supertest 기술
Supertest는 고급 API 테스트를 위한 다양한 기능을 제공합니다. 그 중 몇 가지를 살펴보겠습니다.
1. 요청 본문(Body) 보내기
요청 본문에 데이터를 보내려면 .send()
메서드를 사용할 수 있습니다. 예를 들어, JSON 데이터를 받는 엔드포인트를 만들어 봅시다:
app.post('/users', express.json(), (req, res) => {
const { name, email } = req.body;
// 데이터베이스에 사용자 생성을 시뮬레이션
const user = { id: Date.now(), name, email };
res.status(201).json(user);
});
다음은 Supertest를 사용하여 이 엔드포인트를 테스트하는 방법입니다:
describe('POST /users', () => {
it('creates a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john.doe@example.com',
};
const response = await request(app)
.post('/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
expect(response.body.email).toBe(userData.email);
});
});
설명:
.post('/users')
를 사용하여/users
엔드포인트로 POST 요청을 보냅니다..send(userData)
를 사용하여 요청 본문에userData
객체를 보냅니다. Supertest는 자동으로Content-Type
헤더를application/json
으로 설정합니다..expect(201)
을 사용하여 응답 상태 코드가 201 Created인지 단언합니다.expect(response.body).toHaveProperty('id')
를 사용하여 응답 본문에id
속성이 포함되어 있는지 단언합니다.expect(response.body.name).toBe(userData.name)
및expect(response.body.email).toBe(userData.email)
을 사용하여 응답 본문의name
과email
속성이 요청으로 보낸 데이터와 일치하는지 단언합니다.
2. 헤더(Header) 설정하기
요청에 사용자 정의 헤더를 설정하려면 .set()
메서드를 사용할 수 있습니다. 이는 인증 토큰, 콘텐츠 타입 또는 기타 사용자 정의 헤더를 설정하는 데 유용합니다.
describe('GET /protected', () => {
it('requires authentication', async () => {
const response = await request(app).get('/protected').expect(401);
});
it('returns 200 OK with a valid token', async () => {
// 유효한 토큰을 얻는 것을 시뮬레이션
const token = 'valid-token';
const response = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.text).toBe('Protected Resource');
});
});
설명:
.set('Authorization', `Bearer ${token}`)
을 사용하여Authorization
헤더를Bearer ${token}
으로 설정합니다.
3. 쿠키(Cookie) 처리하기
Supertest는 쿠키도 처리할 수 있습니다. .set('Cookie', ...)
메서드를 사용하여 쿠키를 설정하거나, .cookies
속성을 사용하여 쿠키에 접근하고 수정할 수 있습니다.
4. 파일 업로드 테스트하기
Supertest는 파일 업로드를 처리하는 API 엔드포인트를 테스트하는 데 사용될 수 있습니다. .attach()
메서드를 사용하여 요청에 파일을 첨부할 수 있습니다.
5. 단언 라이브러리 사용하기 (Chai)
Jest의 내장 단언 라이브러리가 많은 경우에 충분하지만, Supertest와 함께 Chai와 같은 더 강력한 단언 라이브러리를 사용할 수도 있습니다. Chai는 더 표현력 있고 유연한 단언 구문을 제공합니다. Chai를 사용하려면 먼저 설치해야 합니다:
npm install --save-dev chai
그런 다음, 테스트 파일로 Chai를 가져와서 해당 단언을 사용할 수 있습니다:
const request = require('supertest');
const app = require('./app');
const chai = require('chai');
const expect = chai.expect;
describe('GET /hello', () => {
it('responds with 200 OK and returns "Hello, World!"', async () => {
const response = await request(app).get('/hello');
expect(response.statusCode).to.equal(200);
expect(response.text).to.equal('Hello, World!');
});
});
참고: Jest가 Chai와 올바르게 작동하도록 설정해야 할 수도 있습니다. 이는 종종 Chai를 가져와 Jest의 전역 expect
와 함께 작동하도록 설정하는 파일을 추가하는 것을 포함합니다.
6. 에이전트(Agent) 재사용하기
특정 환경(예: 인증) 설정이 필요한 테스트의 경우, Supertest 에이전트를 재사용하는 것이 종종 유용합니다. 이렇게 하면 각 테스트 케이스에서 중복된 설정 코드를 피할 수 있습니다.
describe('Authenticated API Tests', () => {
let agent;
beforeAll(() => {
agent = request.agent(app); // 영구적인 에이전트 생성
// 인증 시뮬레이션
return agent
.post('/login')
.send({ username: 'testuser', password: 'password123' });
});
it('can access a protected resource', async () => {
const response = await agent.get('/protected').expect(200);
expect(response.text).toBe('Protected Resource');
});
it('can perform other actions that require authentication', async () => {
// 여기서 다른 인증된 작업 수행
});
});
이 예제에서는 beforeAll
훅에서 Supertest 에이전트를 생성하고 인증합니다. 그러면 describe
블록 내의 후속 테스트들은 각 테스트마다 다시 인증할 필요 없이 이 인증된 에이전트를 재사용할 수 있습니다.
Supertest를 이용한 API 통합 테스트 모범 사례
효과적인 API 통합 테스트를 보장하려면 다음 모범 사례를 고려하십시오:
- 엔드투엔드(End-to-End) 워크플로우 테스트: 격리된 API 엔드포인트보다는 완전한 사용자 워크플로우를 테스트하는 데 집중하세요. 이는 개별 API를 독립적으로 테스트할 때는 드러나지 않을 수 있는 통합 문제를 식별하는 데 도움이 됩니다.
- 현실적인 데이터 사용: 실제 시나리오를 시뮬레이션하기 위해 테스트에 현실적인 데이터를 사용하세요. 여기에는 유효한 데이터 형식, 경계값 및 오류 처리를 테스트하기 위한 잠재적으로 유효하지 않은 데이터 사용이 포함됩니다.
- 테스트 격리: 테스트가 서로 독립적이며 공유 상태에 의존하지 않도록 하십시오. 이렇게 하면 테스트가 더 안정적이고 디버깅하기 쉬워집니다. 전용 테스트 데이터베이스를 사용하거나 외부 종속성을 모의(mocking)하는 것을 고려하세요.
- 외부 종속성 모의(Mock): 데이터베이스, 서드파티 API 또는 기타 서비스와 같은 외부 종속성으로부터 API를 격리하기 위해 모의(mocking)를 사용하세요. 이렇게 하면 테스트가 더 빠르고 안정적이 되며, 외부 서비스의 가용성에 의존하지 않고도 다양한 시나리오를 테스트할 수 있습니다.
nock
과 같은 라이브러리는 HTTP 요청을 모의하는 데 유용합니다. - 포괄적인 테스트 작성: 긍정적 테스트(성공적인 응답 검증), 부정적 테스트(오류 처리 검증) 및 경계 테스트(엣지 케이스 검증)를 포함하여 포괄적인 테스트 커버리지를 목표로 하세요.
- 테스트 자동화: API 통합 테스트를 지속적 통합(CI) 파이프라인에 통합하여 코드베이스가 변경될 때마다 자동으로 실행되도록 하세요. 이는 통합 문제를 조기에 식별하고 프로덕션에 도달하는 것을 방지하는 데 도움이 됩니다.
- 테스트 문서화: API 통합 테스트를 명확하고 간결하게 문서화하세요. 이렇게 하면 다른 개발자들이 테스트의 목적을 이해하고 시간이 지나도 유지 관리하기가 더 쉬워집니다.
- 환경 변수 사용: API 키, 데이터베이스 비밀번호 및 기타 구성 값과 같은 민감한 정보를 테스트에 하드코딩하는 대신 환경 변수에 저장하세요. 이렇게 하면 테스트가 더 안전해지고 다양한 환경에 맞게 구성하기가 더 쉬워집니다.
- API 계약 고려: API 계약 테스트를 활용하여 API가 정의된 계약(예: OpenAPI/Swagger)을 준수하는지 확인하세요. 이는 여러 서비스 간의 호환성을 보장하고 브레이킹 체인지를 방지하는 데 도움이 됩니다. Pact와 같은 도구를 계약 테스트에 사용할 수 있습니다.
피해야 할 일반적인 실수
- 테스트를 격리하지 않음: 테스트는 독립적이어야 합니다. 다른 테스트의 결과에 의존하는 것을 피하세요.
- 구현 세부 사항 테스트: 내부 구현이 아닌 API의 동작과 계약에 집중하세요.
- 오류 처리 무시: API가 유효하지 않은 입력, 엣지 케이스 및 예기치 않은 오류를 어떻게 처리하는지 철저히 테스트하세요.
- 인증 및 권한 부여 테스트 건너뛰기: 무단 접근을 방지하기 위해 API의 보안 메커니즘이 제대로 테스트되었는지 확인하세요.
결론
API 통합 테스트는 소프트웨어 개발 프로세스의 필수적인 부분입니다. Supertest를 사용하면 애플리케이션의 품질과 안정성을 보장하는 데 도움이 되는 포괄적이고 신뢰할 수 있는 API 통합 테스트를 쉽게 작성할 수 있습니다. 엔드투엔드 워크플로우 테스트, 현실적인 데이터 사용, 테스트 격리, 테스트 프로세스 자동화에 집중하는 것을 기억하세요. 이러한 모범 사례를 따르면 통합 문제의 위험을 크게 줄이고 더 견고하고 신뢰할 수 있는 제품을 제공할 수 있습니다.
API가 현대 애플리케이션과 마이크로서비스 아키텍처를 계속 주도함에 따라, 견고한 API 테스트, 특히 통합 테스트의 중요성은 계속해서 커질 것입니다. Supertest는 전 세계 개발자들이 API 상호 작용의 신뢰성과 품질을 보장할 수 있도록 강력하고 접근하기 쉬운 도구 모음을 제공합니다.