본문으로 건너뛰기
김신건의 로그

[Koa] Testing (Supertest, Jest, Vitest)

· 수정 · 📖 약 1분 · 345자/단어 #koa #testing #supertest #jest
Koa Testing, Koa Jest, Koa Supertest, Koa Vitest, Koa 테스트

정의

Koa TestingSupertest (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 을 격리된 형태로 만…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기