SuperTest 치트시트
SuperTest 치트시트
설치
| 플랫폼 | 명령어 |
|---|---|
| npm (All Platforms) | npm install supertest --save-dev |
| Yarn (All Platforms) | yarn add supertest --dev |
| With Jest | npm install jest supertest --save-dev |
| With Mocha | npm install mocha supertest --save-dev |
| With TypeScript | npm install supertest @types/supertest --save-dev |
| Specific Version | npm install supertest@6.3.3 --save-dev |
기본 HTTP 메서드
| 명령어 | 설명 |
|---|---|
request(app).get('/api/users') | 엔드포인트에 GET 요청 보내기 |
request(app).post('/api/users') | 엔드포인트로 POST 요청 보내기 |
request(app).put('/api/users/123') | 리소스를 업데이트하기 위해 PUT 요청을 보냅니다 |
request(app).patch('/api/users/123') | 부분 업데이트를 위해 PATCH 요청을 보내세요 |
request(app).delete('/api/users/123') | 리소스를 제거하기 위해 DELETE 요청을 보냅니다 |
request(app).head('/api/users') | HEAD 요청 보내기 (헤더만) |
request(app).options('/api/users') | CORS 프리플라이트를 위한 OPTIONS 요청 보내기 |
.send({ name: 'John' }) | 요청 본문에 JSON 데이터 보내기 |
.query({ page: 1, limit: 10 }) | URL에 쿼리 매개변수 추가하기 |
.set('Authorization', 'Bearer token') | 요청 헤더 설정 |
.expect(200) | HTTP 상태 코드 확인 |
.expect('Content-Type', /json/) | 응답 헤더 값 확인 |
.end((err, res) => {}) | 콜백으로 요청 실행하기 |
await request(app).get('/api/users') | Promise/async-await로 요청 실행하기 |
어서션 및 기대값
| 명령어 | 설명 |
|---|---|
.expect(200) | 정확한 상태 코드를 예상하세요 |
.expect(200, { id: 1 }) | 상태 및 정확한 본문 일치를 예상하세요 |
.expect('Content-Type', 'application/json') | 정확한 헤더 값을 예상하세요 |
.expect('Content-Type', /json/) | 헤더 매칭 정규식을 예상하세요 |
.expect((res) => { if (!res.body) throw new Error() }) | 사용자 정의 어설션 함수 |
.expect(200, /success/) | 상태와 본문이 정규식과 일치하는 것을 예상하세요 |
.expect('Location', '/new-url') | 리다이렉트 위치 헤더를 예상하세요 |
.expect({ error: 'Not found' }) | 정확한 JSON 본문 일치를 예상하세요 |
요청 구성
| 명령어 | 설명 |
|---|---|
.set('Authorization', 'Bearer token') | 단일 요청 헤더 설정 |
.set({ 'X-API-Key': 'key', 'Accept': 'json' }) | 여러 헤더 설정하기 |
.auth('username', 'password') | HTTP 기본 인증 설정 |
.field('name', 'value') | 다중 부분(multipart) 양식 필드 추가 |
.attach('file', 'path/to/file.pdf') | 업로드를 위한 파일 첨부 |
.attach('file', buffer, 'filename.pdf') | 버퍼에서 파일 첨부 |
.timeout(5000) | 밀리초 단위로 요청 시간 초과 설정 |
.redirects(5) | 최대 5개의 리디렉션 따라가기 |
.type('json') | Content-Type 헤더 설정 |
.accept('json') | Accept 헤더 설정 |
.send('raw string data') | 원시 문자열 본문 보내기 |
.send(Buffer.from('data')) | 이진 데이터 보내기 |
고급 사용법
| 명령어 | 설명 |
|---|---|
request.agent(app) | 지속적인 쿠키를 위한 에이전트 생성 |
agent.get('/api/profile') | 인증된 요청에 에이전트 사용 |
request('https://api.example.com') | 외부 API 엔드포인트 테스트 |
.parse((res, callback) => {}) | 맞춤형 응답 파서 |
.buffer(true) | 버퍼 응답 본문 |
.responseType('blob') | 예상되는 응답 유형 설정 |
.retry(3) | 실패한 요청을 3번 재시도하기 |
.ca(cert) | 사용자 지정 CA 인증서 설정 |
.cert(cert) | 클라이언트 인증서 설정 |
.key(key) | 클라이언트 개인 키 설정 |
.pfx(pfxData) | PFX/PKCS12 인증서 설정 |
.disableTLSCerts() | TLS 인증서 검증 비활성화 |
테스트 패턴
| 패턴 | 설명 |
|---|---|
const res = await request(app).get('/api/users') | 여러 어설션에 대한 응답 저장 |
expect(res.body).toHaveLength(10) | 테스트 프레임워크로 응답 본문에 대해 어설트(Assert)하기 |
expect(res.headers['x-custom']).toBe('value') | 응답 헤더에 대해 어설트(Assert)하기 |
expect(res.status).toBe(200) | 대체 상태 어설션 |
Promise.all([request(app).get('/a'), request(app).get('/b')]) | 병렬 요청 테스팅 |
for (const user of users) { await request(app).post('/api/users').send(user) } | 순차적 테스트 요청 |
구성
기본 테스트 설정
// test/setup.js
const request = require('supertest');
const app = require('../app');
module.exports = { request, app };
Jest 구성
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
collectCoverageFrom: ['src/**/*.js'],
coveragePathIgnorePatterns: ['/node_modules/']
};
Mocha 구성
// test/mocha.opts
--require test/setup.js
--recursive
--timeout 5000
--exit
Package.json 스크립트
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:api": "jest --testPathPattern=api"
}
}
환경 변수
// config/test.js
module.exports = {
port: process.env.TEST_PORT || 3001,
database: process.env.TEST_DB || 'test_db',
apiTimeout: process.env.API_TIMEOUT || 5000
};
일반적인 사용 사례
사용 사례 1: REST API CRUD 작업 테스트
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
let userId;
// Create
test('POST /api/users - create user', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'John Doe', email: 'john@example.com' })
.expect(201)
.expect('Content-Type', /json/);
userId = res.body.id;
expect(res.body.name).toBe('John Doe');
});
// Read
test('GET /api/users/:id - get user', async () => {
await request(app)
.get(`/api/users/${userId}`)
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('John Doe');
});
});
// Update
test('PUT /api/users/:id - update user', async () => {
await request(app)
.put(`/api/users/${userId}`)
.send({ name: 'Jane Doe' })
.expect(200);
});
// Delete
test('DELETE /api/users/:id - delete user', async () => {
await request(app)
.delete(`/api/users/${userId}`)
.expect(204);
});
});
사용 사례 2: 인증 흐름 테스트
const request = require('supertest');
const app = require('../app');
describe('Authentication', () => {
let authToken;
test('POST /auth/register - register new user', async () => {
await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
})
.expect(201);
});
test('POST /auth/login - login user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
})
.expect(200)
.expect('Content-Type', /json/);
authToken = res.body.token;
expect(authToken).toBeDefined();
});
test('GET /api/profile - access protected route', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
});
});
test('GET /api/profile - reject without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
});
사용 사례 3: 파일 업로드 테스트
const request = require('supertest');
const app = require('../app');
const path = require('path');
describe('File Upload', () => {
test('POST /api/upload - upload single file', async () => {
const filePath = path.join(__dirname, 'fixtures', 'test.pdf');
const res = await request(app)
.post('/api/upload')
.attach('document', filePath)
.field('title', 'Test Document')
.field('description', 'A test file upload')
.expect(200)
.expect('Content-Type', /json/);
expect(res.body.filename).toMatch(/\.pdf$/);
expect(res.body.size).toBeGreaterThan(0);
});
test('POST /api/upload - upload multiple files', async () => {
await request(app)
.post('/api/upload/multiple')
.attach('files', 'test/fixtures/file1.pdf')
.attach('files', 'test/fixtures/file2.pdf')
.expect(200)
.expect((res) => {
expect(res.body.files).toHaveLength(2);
});
});
});
사용 사례 4: 지속적인 세션으로 테스트
const request = require('supertest');
const app = require('../app');
describe('Session Management', () => {
const agent = request.agent(app);
test('Login and maintain session', async () => {
// Login
await agent
.post('/auth/login')
.send({ username: 'user', password: 'pass' })
.expect(200);
// Session persists - can access protected route
await agent
.get('/api/dashboard')
.expect(200);
// Session still valid for subsequent requests
await agent
.get('/api/profile')
.expect(200);
// Logout
await agent
.post('/auth/logout')
.expect(200);
// Session expired - access denied
await agent
.get('/api/dashboard')
.expect(401);
});
});
사용 사례 5: 오류 처리 테스트
const request = require('supertest');
const app = require('../app');
describe('Error Handling', () => {
test('404 - Resource not found', async () => {
await request(app)
.get('/api/users/99999')
.expect(404)
.expect((res) => {
expect(res.body.error).toBe('User not found');
expect(res.body.code).toBe('USER_NOT_FOUND');
});
});
test('400 - Validation error', async () => {
await request(app)
.post('/api/users')
.send({ name: '' }) // Invalid data
.expect(400)
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors.name).toContain('required');
});
});
test('429 - Rate limit exceeded', async () => {
// Make multiple requests to trigger rate limit
const requests = Array(101).fill().map(() =>
request(app).get('/api/users')
);
const responses = await Promise.all(requests);
const rateLimited = responses.find(r => r.status === 429);
expect(rateLimited).toBeDefined();
expect(rateLimited.body.error).toMatch(/rate limit/i);
});
test('500 - Internal server error', async () => {
await request(app)
.get('/api/trigger-error')
.expect(500)
.expect((res) => {
expect(res.body.error).toBe('Internal server error');
});
});
});
모범 사례
-
async/await 구문 사용: 콜백보다 더 깔끔하고 가독성이 높습니다. 최신 JavaScript는 이 패턴을 기본적으로 지원합니다.
const res = await request(app).get('/api/users').expect(200); -
개발 의존성으로 설치: SuperTest는 테스트 도구이므로
devDependencies에만 있어야 하며, 프로덕션 의존성에는 포함되지 않습니다.```bash npm install supertest —save-dev -
Don’t start the server manually: SuperTest handles server binding automatically. Pass the Express app directly without calling
app.listen().// Good: Export app without listening module.exports = app; // Bad: Don't do this in test files app.listen(3000); -
Use request.agent() for session testing: When testing authenticated routes or cookie-based sessions, create an agent to persist cookies across requests.
const agent = request.agent(app); await agent.post('/login').send(credentials); await agent.get('/protected'); // Cookies persist -
Chain expectations for cleaner tests: Multiple
.expect()calls can be chained for comprehensive assertions in a single test.await request(app) .get('/api/users') .expect(200) .expect('Content-Type', /json/) .expect((res) => expect(res.body).toHaveLength(10)); -
Store responses for complex assertions: Capture the response object when you need to perform multiple assertions on different parts.
const res = await request(app).get('/api/users').expect(200); expect(res.body).toHaveLength(5); expect(res.headers['x-total-count']).toBe('100'); -
Test both success and failure cases: Always test error scenarios, validation failures, and edge cases alongside happy paths.
test('returns 404 for non-existent user', async () => { await request(app).get('/api/users/99999').expect(404); }); -
Use descriptive test names: Write test descriptions that clearly explain what behavior is being verified.
test('POST /api/users returns 400 when email is missing', async () => { // Test implementation }); -
Clean up test data: Use
beforeEach/afterEachhooks to reset database state and ensure test isolation.afterEach(async () => { await User.deleteMany({}); }); -
Set appropriate timeouts: For slow endpoints or external API calls, configure timeout values to prevent false failures.
await request(app).get('/api/slow').timeout(10000).expect(200);
Troubleshooting
| 문제 | 솔루션 |
|---|---|
EADDRINUSE: address already in use | Don’t call app.listen() in your app file when testing. Export the app without starting the server, or use different ports for testing. |
| Tests hang and never complete | Ensure database connections are closed after tests. Use afterAll() to close connections: afterAll(() => mongoose.connection.close()) |
Cannot read property 'body' of undefined | Missing await keyword or .end() callback. Always use await with async/await or provide .end() callback. |
| Headers already sent error | You’re sending multiple responses in your route handler. Ensure only one res.send(), res.json(), or similar call per request. |
| Timeout errors on slow endpoints | Increase timeout with .timeout(ms) or adjust global timeout in test framework config: jest.setTimeout(10000) |
| Authentication tests fail randomly | Use request.agent() to persist cookies/sessions across requests instead of separate request() calls. |
| File upload tests fail | Verify file path is correct using path.join(__dirname, 'fixtures', 'file.pdf'). Ensure multipart middleware is configured in app. |
| CORS errors in tests | CORS is typically a browser concern. SuperTest bypasses CORS. If testing CORS headers, use .options() request with Origin header. |
| SSL/TLS certificate errors | Use .disableTLSCerts() for testing environments or provide valid certificates with .ca(), .cert(), .key() methods. |
| Response body is empty Buffer | Response might be binary. Use .buffer(true) or check Content-Type. For JSON, ensure server sends correct Content-Type header. |
| Tests pass individually but fail together | Tests aren’t isolated. Clear database/state between tests using beforeEach/afterEach hooks. Check for shared mutable state. |