惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
博客园 - 司徒正美
L
LangChain Blog
有赞技术团队
有赞技术团队
大猫的无限游戏
大猫的无限游戏
Stack Overflow Blog
Stack Overflow Blog
Engineering at Meta
Engineering at Meta
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
月光博客
月光博客
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
D
DataBreaches.Net

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I Test My Node.js Apps: A Practical Guide (2026)
Alex Chen · 2026-05-17 · via DEV Community

Alex Chen

How I Test My Node.js Apps: A Practical Guide (2026)

Testing doesn't have to be complicated. Here's the minimal setup that catches real bugs.

The Truth About Testing

Most developers know they should test.
Few actually do it consistently.

Why?
→ "I don't have time"
→ "My code is too simple to test"
→ "Setting up tests is annoying"
→ "Tests break when I refactor"

Here's my counter-argument:
→ Tests catch bugs before users do
→ Tests document how your code works
→ Tests give you confidence to refactor
→ The "minimal" setup takes 15 minutes

Enter fullscreen mode Exit fullscreen mode

My Testing Stack

Layer Tool Why
Unit tests Node.js built-in test runner Zero dependencies
HTTP APIs node:test + undici dispatch Built into Node.js 18+
Integration Custom scripts with supertest Simple, predictable
E2E Playwright Reliable, fast, free

Total cost: $0. Everything here is free and open source.

Step 1: Unit Testing with Node.js Built-in Runner

No need for Jest or Mocha anymore. Node.js has its own test runner since v18:

# No installation needed — it's built in
node --test

Enter fullscreen mode Exit fullscreen mode

Basic Example

// math.js
export function add(a, b) {
  return a + b;
}

export function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

Enter fullscreen mode Exit fullscreen mode

// math.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { add, divide } from './math.js';

describe('Math utilities', () => {
  it('should add two numbers', () => {
    assert.equal(add(2, 3), 5);
  });

  it('should handle negative numbers', () => {
    assert.equal(add(-1, 1), 0);
  });

  it('should throw on division by zero', () => {
    assert.throws(() => divide(10, 0), {
      message: 'Division by zero'
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

$ node --test
✓ Math utilities (4.8ms)
✓ should add two numbers
✓ should handle negative numbers
✓ should throw on division by zero

ℹ tests 3
ℹ pass 3
ℹ fail 0
ℹ duration 15ms

Enter fullscreen mode Exit fullscreen mode

Async Code Testing

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

describe('API calls', () => {
  it('should fetch data successfully', async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();

    assert.ok(data.items);
    assert(Array.isArray(data.items));
  });

  it('should timeout after 5 seconds', async () => {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 5000);

    await assert.rejects(
      async () => fetch('https://slow-api.example.com', {
        signal: controller.signal
      }),
      { name: 'AbortError' }
    );
  });
});

Enter fullscreen mode Exit fullscreen mode

Step 2: Testing Express.js APIs

This is where most side projects need testing:

// app.js
import express from 'express';

const app = express();
app.use(express.json());

let items = [];

app.get('/api/items', (req, res) => {
  res.json({ items });
});

app.post('/api/items', (req, res) => {
  const { name } = req.body;
  if (!name) return res.status(400).json({ error: 'Name required' });

  const item = { id: Date.now(), name };
  items.push(item);
  res.status(201).json(item);
});

export default app;

Enter fullscreen mode Exit fullscreen mode

// app.test.js
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import createApp from './app.js';

describe('Items API', () => {
  let app;

  beforeEach(() => {
    app = createApp();
  });

  it('GET /api/items returns empty array initially', async () => {
    // We'll use undici's MockAgent for mocking HTTP
    // Or use supertest-style approach
  });

  it('POST /api/items creates an item', async () => {
    // Test implementation below
  });
});

Enter fullscreen mode Exit fullscreen mode

Using Supertest for API Testing

npm install --save-dev supertest

Enter fullscreen mode Exit fullscreen mode

// app.test.js (with supertest)
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import createApp from './app.js';

describe('Items API', () => {
  let app;

  beforeEach(() => {
    app = createApp();
  });

  it('GET /api/items returns empty array initially', async () => {
    const res = await request(app).get('/api/items');
    assert.equal(res.status, 200);
    assert.deepEqual(res.body, { items: [] });
  });

  it('POST /api/item creates item with valid data', async () => {
    const res = await request(app)
      .post('/api/items')
      .send({ name: 'Test Item' })
      .expect('Content-Type', /json/);

    assert.equal(res.status, 201);
    assert.equal(res.body.name, 'Test Item');
    assert.ok(res.body.id);
  });

  it('POST /api/items rejects missing name', async () => {
    const res = await request(app)
      .post('/api/items')
      .send({})
      .expect(400);

    assert.equal(res.body.error, 'Name required');
  });
});

Enter fullscreen mode Exit fullscreen mode

Step 3: Testing Database Operations

For SQLite (what I use in production):

// db.test.js
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';

const TEST_DB_PATH = path.join('/tmp', `test-${Date.now()}.db`);

describe('Database operations', () => {
  let db;

  beforeEach(() => {
    db = new Database(TEST_DB_PATH);
    db.exec(`CREATE TABLE users (
      id INTEGER PRIMARY KEY,
      email TEXT UNIQUE NOT NULL,
      created_at TEXT DEFAULT CURRENT_TIMESTAMP
    )`);
  });

  afterEach(() => {
    db.close();
    try { fs.unlinkSync(TEST_DB_PATH); } catch {}
  });

  it('inserts and retrieves a user', () => {
    const stmt = db.prepare('INSERT INTO users (email) VALUES (?)');
    const result = stmt.run('test@example.com');

    assert.ok(result.lastInsertRowid);

    const user = db.prepare('SELECT * FROM users WHERE id = ?').get(result.lastInsertRowid);
    assert.equal(user.email, 'test@example.com');
  });

  it('rejects duplicate emails', () => {
    const stmt = db.prepare('INSERT INTO users (email) VALUES (?)');
    stmt.run('unique@example.com');

    assert.throws(() => {
      stmt.run('unique@example.com');
    }, /UNIQUE constraint failed/);
  });
});

Enter fullscreen mode Exit fullscreen mode

Key pattern: Use a fresh database file for each test run (Date.now() ensures uniqueness).

Step 4: Running Tests Automatically

package.json Scripts

{
  "scripts": {
    "test": "node --test",
    "test:watch": "node --test --watch",
    "test:coverage": "node --experimental-test-coverage --test"
  }
}

Enter fullscreen mode Exit fullscreen mode

With GitHub Actions (Free CI)

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm install
      - run: npm test

Enter fullscreen mode Exit fullscreen mode

This runs your tests on 3 Node.js versions every push. Free for public repos.

What I Actually Test (And What I Skip)

✅ TEST thoroughly:
   → Business logic (calculations, validations, transformations)
   → API endpoints (request/response format, error handling)
   → Database queries (CRUD operations, constraints)
   → Authentication flows (login, token validation)

⚠️ TEST lightly:
   → Utility functions (simple pure functions)
   → Configuration loading
   → Basic CRUD scaffolding

❌ SKIP (usually):
   → UI/CSS (use visual testing tools instead)
   → Third-party library behavior (they have their own tests)
   → Static content pages

Enter fullscreen mode Exit fullscreen mode

The 80/20 Rule of Testing

20% of your code handles 80% of the complexity → TEST THIS PART

Focus on:
1. Conditional logic (if/else, switch/case)
2. Error handling paths
3. Data transformations
4. External API integrations
5. Authentication & authorization

Don't waste time testing getters/setters or trivial components.

Enter fullscreen mode Exit fullscreen mode

Common Mistakes I Made (So You Don't Have To)

Mistake #1: Testing Implementation Details

// ❌ Bad — tests internal structure
it('calls save() once', () => {
  assert.equal(mockSave.callCount, 1);
});

// ✅ Good — tests observable behavior
it('persists data correctly', () => {
  const saved = db.query('SELECT * FROM users');
  assert.equal(saved.length, 1);
});

Enter fullscreen mode Exit fullscreen mode

Mistake #2: Over-mocking

// ❌ Bad — mocking everything makes tests fragile
const mockDb = { query: mockFn, exec: mockFn, prepare: mockFn };

// ✅ Good — use real database (SQLite is fast enough)
const db = new Database(':memory:');

Enter fullscreen mode Exit fullscreen mode

Mistake #3: Ignoring Error Paths

// Most developers only test happy path
it('returns user data', () => { ... });

// Also test what happens when things go WRONG
it('handles database connection failure', () => { ... });
it('returns 404 for non-existent user', () => { ... });
it('validates malformed input', () => { ... });

Enter fullscreen mode Exit fullscreen mode

Quick Reference: Assertion Cheatsheet

import assert from 'node:assert/strict';

// Equality
assert.equal(actual, expected);       // ==
assert.notEqual(actual, expected);    // !=
assert.deepEqual(actual, expected);   // deep object compare

// Types & existence
assert.ok(value);                     // truthy
assert.strictEqual(a, b);             // ===
assert.typeOf(value, 'string');       // type check

// Exceptions
assert.throws(fn, { message: /.../ }); // must throw matching error
assert.doesNotThrow(fn);              // must not throw

// Promises
await assert.rejects(asyncFn);        // promise must reject
await assert.doesNotReject(asyncFn);  // promise must resolve

Enter fullscreen mode Exit fullscreen mode


What's your testing setup? Are you using the built-in test runner yet?

Follow @armorbreak for more practical Node.js guides.


Resources: