Chuyển đến nội dung chính

レッスン 17: Node.js アプリケーションのテスト

Node.js 組み込みテスト ランナー (node:test)。単体テスト/統合テスト用の Vitest。 HTTP テスト用のスーパーテスト。 HTTP モックの場合はノックします。テストコンテナ、コードカバレッジ (c8/istanbul)。

💻 プログラミング — レッスン 17 レッスン 17: Node.js アプリケーションのテスト

Node.js コア: 基本から高度まで

パート 5: テスト、パフォーマンス、および実稼働

xdev.asia

1. 組み込みテストランナー (node:test)

import { describe, it, before, after, mock } from 'node:test'
import assert from 'node:assert/strict'

describe('UserService', () => {
  let db: Database

  before(async () => {
    db = await Database.connect(':memory:')
    await db.migrate()
  })

  after(async () => {
    await db.close()
  })

  it('creates a user', async () => {
    const user = await createUser(db, { name: 'Test', email: '[email protected]' })
    assert.strictEqual(user.name, 'Test')
    assert.ok(user.id)
  })

  it('rejects duplicate email', async () => {
    await assert.rejects(
      () => createUser(db, { name: 'Test2', email: '[email protected]' }),
      { message: /unique constraint/i }
    )
  })

  it('mocks external service', async () => {
    const sendEmail = mock.fn(async () => ({ success: true }))
    const result = await sendEmail('[email protected]', 'Hello')
    assert.strictEqual(sendEmail.mock.calls.length, 1)
  })
})
node --test --test-reporter spec src/**/*.test.ts

2. ヴィテスト

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      include: ['src/**'],
      exclude: ['src/**/*.test.ts'],
    },
  },
})
// src/services/user.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'

describe('UserService', () => {
  const mockDb = {
    query: vi.fn(),
  }

  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('finds user by id', async () => {
    mockDb.query.mockResolvedValue({
      rows: [{ id: '1', name: 'Test' }]
    })

    const user = await findUser(mockDb as any, '1')
    expect(user).toEqual({ id: '1', name: 'Test' })
    expect(mockDb.query).toHaveBeenCalledWith(
      expect.stringContaining('SELECT'),
      ['1']
    )
  })
})

3. HTTP テスト (スーパーテスト)

import request from 'supertest'
import { createApp } from '../src/app'

const app = createApp()

describe('API', () => {
  it('GET /api/users returns users', async () => {
    const res = await request(app)
      .get('/api/users')
      .expect(200)
      .expect('Content-Type', /json/)

    expect(res.body).toBeInstanceOf(Array)
    expect(res.body[0]).toHaveProperty('name')
  })

  it('POST /api/users creates user', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'New User', email: '[email protected]' })
      .expect(201)

    expect(res.body.name).toBe('New User')
  })

  it('returns 401 without auth', async () => {
    await request(app)
      .get('/api/admin')
      .expect(401)
  })
})

4. HTTP モッキング (Nock)

import nock from 'nock'

it('handles external API', async () => {
  nock('https://api.github.com')
    .get('/users/octocat')
    .reply(200, { login: 'octocat', id: 1 })

  const user = await fetchGitHubUser('octocat')
  expect(user.login).toBe('octocat')
})

5. テストコンテナ

import { PostgreSqlContainer } from '@testcontainers/postgresql'

let container: any
let connectionString: string

beforeAll(async () => {
  container = await new PostgreSqlContainer().start()
  connectionString = container.getConnectionUri()
  await runMigrations(connectionString)
}, 30000)

afterAll(async () => {
  await container.stop()
})

次の記事: パフォーマンスのプロファイリングと最適化 — V8 プロファイラー、フレームグラフ、Clinic.js。