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

Lesson 17: Testing Node.js Applications

Node.js built-in test runner (node:test). Vitest for unit/integration tests. Supertest for HTTP testing. Nock for HTTP mocking. Testcontainers, code coverage (c8/istanbul).

💻 Programming — Lesson 17 Lesson 17: Testing Node.js Applications

Node.js Core: From Basics to Advanced

Part 5: Testing, Performance & Production

xdev.asia

1. Built-in Test Runner (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

// 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 Testing (Supertest)

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 Mocking (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. Testcontainers

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()
})

Next article: Performance Profiling & Optimization — V8 profiler, flamegraphs, Clinic.js.