[Koa] Testing (Supertest, Jest, Vitest)
Koa Testing, Koa Jest, Koa Supertest, Koa Vitest, Koa 테스트
정의
Koa Testing 은 Supertest (HTTP 호출) + Jest/Vitest (assertions/runner) 조합이 표준입니다. app.callback() 을 Supertest 에 전달하면 실제 HTTP 서버 없이 요청/응답 검증 가능.
설치
npm i -D jest supertest
# 또는
npm i -D vitest supertest
기본 예시
// app.js
const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('@koa/bodyparser');
function createApp() {
const app = new Koa();
const router = new Router();
router.get('/', async (ctx) => {
ctx.body = 'Hello';
});
router.get('/users/:id', async (ctx) => {
ctx.body = {id: ctx.params.id};
});
router.post('/users', async (ctx) => {
ctx.assert(ctx.request.body.email, 400, 'email required');
ctx.status = 201;
ctx.body = ctx.request.body;
});
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());
return app;
}
module.exports = createApp;
// app.test.js
const request = require('supertest');
const createApp = require('./app');
describe('App', () => {
let app;
beforeEach(() => {
app = createApp();
});
it('GET / returns Hello', async () => {
const res = await request(app.callback())
.get('/')
.expect(200);
expect(res.text).toBe('Hello');
});
it('GET /users/:id returns id', async () => {
const res = await request(app.callback())
.get('/users/42')
.expect(200)
.expect('Content-Type', /json/);
expect(res.body).toEqual({id: '42'});
});
it('POST /users creates user', async () => {
const res = await request(app.callback())
.post('/users')
.send({email: 'a@b.com', name: 'kim'})
.expect(201);
expect(res.body.email).toBe('a@b.com');
});
it('POST /users returns 400 without email', async () => {
await request(app.callback())
.post('/users')
.send({name: 'kim'})
.expect(400);
});
});
Supertest API
request(app.callback())
.get('/users?page=1&limit=10')
.set('Authorization', 'Bearer token')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
.expect('X-Custom', 'value')
.expect((res) => {
expect(res.body.length).toBe(10);
});
// POST with body
request(app.callback())
.post('/users')
.send({email: 'a@b.com'}) // JSON 자동
.expect(201);
// Form data
request(app.callback())
.post('/form')
.type('form')
.send({name: 'kim', email: 'a@b.com'});
// File upload
request(app.callback())
.post('/upload')
.attach('file', './test/fixtures/photo.jpg');
// Multiple fields
request(app.callback())
.post('/upload')
.field('name', 'Test')
.attach('file', 'test.jpg');
// Cookies
request(app.callback())
.get('/me')
.set('Cookie', 'session=abc');
Middleware 단위 테스트
Middleware 를 직접 호출:
const requestId = require('./middleware/request-id');
it('sets X-Request-Id', async () => {
const ctx = {
set: jest.fn(),
get: jest.fn().mockReturnValue(''),
state: {},
};
const next = jest.fn();
await requestId()(ctx, next);
expect(ctx.state.requestId).toBeDefined();
expect(ctx.set).toHaveBeenCalledWith('X-Request-Id', expect.any(String));
expect(next).toHaveBeenCalled();
});
Mock ctx 는 필요한 필드만 채움.
인증 mocking
async function mockAuth(user) {
return async (ctx, next) => {
ctx.state.user = user;
await next();
};
}
// 테스트에서 app 재구성
function testApp(overrides = {}) {
const app = createApp();
if (overrides.user) {
app.middleware.unshift(mockAuth(overrides.user));
}
return app;
}
it('GET /me returns current user', async () => {
const app = testApp({user: {id: 1, name: 'kim'}});
const res = await request(app.callback())
.get('/me')
.expect(200);
expect(res.body.id).toBe(1);
});
DB 격리
SQLite in-memory (Prisma)
process.env.DATABASE_URL = 'file::memory:?cache=shared';
beforeAll(async () => {
await runMigrations();
});
Testcontainers (PostgreSQL)
const {PostgreSqlContainer} = require('@testcontainers/postgresql');
let container, db;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16').start();
process.env.DATABASE_URL = container.getConnectionUri();
db = require('./db');
await db.migrate();
}, 60000);
afterAll(async () => {
await db.disconnect();
await container.stop();
});
Transaction rollback per test
let tx;
beforeEach(async () => {
tx = await db.startTransaction();
});
afterEach(async () => {
await tx.rollback();
});
Snapshot testing
it('GET /users matches snapshot', async () => {
const res = await request(app.callback())
.get('/users')
.expect(200);
expect(res.body).toMatchSnapshot();
});
HTTP mocking (외부 API)
nock:
npm i -D nock
const nock = require('nock');
beforeEach(() => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
});
afterEach(() => {
nock.cleanAll();
});
it('handles external API', async () => {
nock('https://api.external.com')
.get('/data/42')
.reply(200, {value: 'test'});
const res = await request(app.callback())
.get('/proxy/42')
.expect(200);
expect(res.body.value).toBe('test');
});
또는 MSW.
Error path 테스트
it('returns 404 for missing user', async () => {
const res = await request(app.callback())
.get('/users/999')
.expect(404);
expect(res.body.error.code).toBe('USER_NOT_FOUND');
});
it('returns 400 on invalid body', async () => {
await request(app.callback())
.post('/users')
.send({email: 'not-an-email'})
.expect(400);
});
it('logs 500 errors', async () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
// Endpoint 가 throw 하도록
await request(app.callback())
.get('/error')
.expect(500);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});
Environment 격리
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
process.env.NODE_ENV = 'test';
});
afterEach(() => {
process.env.NODE_ENV = originalEnv;
});
E2E vs Unit
- Unit: middleware, controller 함수를 직접 mock ctx 로 테스트
- Integration:
app.callback()+ Supertest 로 HTTP 요청 시뮬레이션 - E2E: 실제 서버 기동 + 실제 DB (또는 testcontainers)
Koa 는 app.callback() 이 실 HTTP 서버 없이 요청 처리 가능해 integration test 가 매우 빠릅니다.
CI 통합 (GitHub Actions)
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports: [5432:5432]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test -- --coverage
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
Vitest (더 빠름, ESM 우선)
// vitest.config.js
export default {
test: {
environment: 'node',
globals: true,
coverage: {
reporter: ['text', 'html'],
exclude: ['**/node_modules/**', '**/*.test.js'],
},
},
};
// test 는 Jest 와 거의 동일 API
import {describe, it, expect, beforeEach, vi} from 'vitest';
import request from 'supertest';
함정
WARNING
app.listen() 후 close 안 하면 test 종료 안 됨. app.callback() 을 supertest 에 넘기면 자동. 직접 listen 은 afterAll 에서 close.
CAUTION
DB 상태 leak. 각 테스트 후 rollback 또는 truncate.
WARNING
process.env 조작. Test 간 leak. beforeEach 에서 backup, afterEach 에서 restore.
IMPORTANT
Middleware order 는 test 에도 영향. Auth mock 을 잘못된 위치에 삽입하면 실제 auth 안 우회.
CAUTION
Async assertion. expect(promise).resolves / .rejects 형태.
관련 위키
이 글의 용어 (10개)
- [FastAPI] Testingfastapi
- 정의 FastAPI Testing 은 endpoint 를 실제 서버 없이 직접 호출해 응답을 검증하는 workflow 입니다. Starlette 의 (내부적으로 sync) 또는 …
- [Flask] Testingflask
- 정의 Flask Testing 은 앱 인스턴스를 직접 감싸 HTTP 요청을 시뮬레이션하는 workflow 입니다. (Werkzeug) 로 라우팅/뷰/미들웨어까지 실제 실행하고 응…
- [Framework] Koa.jsframeworks
- 정의 Koa.js 는 Express 창시자 TJ Holowaychuk 이 2013년 발표한 Node.js 웹 프레임워크 입니다. Express 의 후계자 성격이며, 미들웨어를 a…
- [Koa] Body Parsingframeworks
- 정의 Koa Body Parsing 은 HTTP 요청 body 를 파싱해 에 사용 가능한 형태로 만드는 미들웨어입니다. Koa 코어는 body parser 를 포함하지 않으므로 …
- [Koa] Context (ctx)frameworks
- 정의 Koa Context ( ) 는 각 요청마다 생성되어 미들웨어와 route handler 에 전달되는 객체입니다. (Node 원본), (Koa 확장), (Koa 확장), (…
- [Koa] Deployment (PM2, Docker, Kubernetes)frameworks
- 정의 Koa Deployment 는 Node.js 프로덕션 배포의 표준 도구 (Docker, Kubernetes, PM2) + Koa 특화 고려사항 (graceful shutdo…
- [Koa] Error Handlingframeworks
- 정의 Koa Error Handling 은 미들웨어 chain 에서 발생한 예외를 catch 하여 응답을 정형화하는 패턴입니다. Koa 는 자동 error handler 를 제공…
- [Koa] Middleware (Onion Model)frameworks
- 정의 Koa Middleware 는 요청/응답 사이에 실행되는 async 함수입니다. 각 미들웨어는 (context) 와 (다음 미들웨어를 실행하는 함수) 를 받고, 로 down…
- [Koa] Router (@koa/router)frameworks
- 정의 @koa/router 는 Koa 의 공식 라우터입니다. Koa 코어는 라우팅을 제공하지 않으므로 별도 설치가 필요합니다. Express 스타일 라우팅 (HTTP method…
- [NestJS] Testing (Jest, Supertest, e2e)frameworks
- 정의 NestJS Testing 은 Jest (기본) + Supertest 조합으로 unit test 와 e2e test 를 지원합니다. 의 로 module 을 격리된 형태로 만…
💬 댓글