⇦ Back

WDX-180

Web Development X

Testing Express Applications

Trust, But Verify

Every developer believes their code works.

Testing is how we discover whether reality agrees.

Up until now we’ve built:

  CRUD

  Authentication

  Authorization

  Validation

  File Uploads

Everything appears to work.

Then a user discovers:

  Deleting a product deletes the wrong product.

Or:

  Editors can access admin routes.

Or:

  A validation rule accidentally disappeared.

Today’s lesson is about building confidence in software.

Not confidence because:

  "It worked on my machine."

Confidence because:

  Tests proved it.

Learning Objectives

By the end of this lesson, students will be able to:

Part 1 — What Is Testing?

A test is simply code that verifies other code.

Example:

  // Call the function we want to test ->  Check the result against our expectation:
  expect(add(2, 3)).toBe(5);

Question:

  Does add() work?

Test answers:

  Yes

or

  No

without human involvement.

Part 2 — Why Testing Matters

Without tests:

  Change Code

  Hope Nothing Breaks

With tests:

  Change Code

  Run Tests

  Know What Broke

This is a huge difference.

Part 3 — Types of Tests

Three major categories:

Unit Tests

Test one thing.

Example:

  validateProduct()

Our unit tests should test whether the validateProduct is producing the expected output given specific input and behaves appropriately when malformed input is passed into it.

Integration Tests

Test multiple components together.

Example:

  Route

  ↓

  Database

  ↓

  Response

End-to-End Tests

Test the entire application.

Example:

  Browser

  ↓

  Server

  ↓

  Database

For this module we’ll focus on:

  Unit

  Integration

Part 4 — Installing a Test Framework

Popular choices:

We’ll use Vitest.

Install:

  npm install -D vitest

package.json:

  {
    "scripts": {
      "test": "vitest"
    }
  }

Run:

  npm test

Part 5 — Your First Test

Function:

  // utils.js
  function add(a, b) {
      return a + b;
  }
  module.exports = { add }

Test:

  // utils.test.js
  import { test, expect } from 'vitest';
  import { add } from "./utils";

  test( 'adds numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

Now run the tests via npm run test

Result:

  PASS

Or:

  FAIL

No ambiguity.

ALWAYS break the tests, to test your testing logic:

  function add(a, b) {
      return a - b; // Change + to -
  }  

Re-run the tests and make sure they break.

Part 6 — Testing Validation

Validator:

  validateProduct(data)

Test:

  test('requires name', () => {
          const errors = validateProduct({
                  name: '',
                  price: 10
              });
          expect(errors.length).toBe(1);
          expect(errors[0]).toBe('Name is required');
      }
  );

This is a perfect unit test.

No database.

No Express.

No browser.

Just:

  Input

  ↓

  Output

Now challenge yourself by adding more tests on the validateProducts function.

Part 7 — Testing Authorization

Function:

  canDeleteProduct(user)

Test:

  test('admin can delete', () => {
    expect(canDeleteProduct({role: 'admin' })).toBe(true);
  });

Test:

  test('viewer cannot delete', () => {
    expect(canDeleteProduct({ role: 'viewer' })).toBe(false);
  });

Permissions are excellent candidates for unit tests.

Part 8 — Integration Testing Express

Unit tests are great.

But eventually we need:

  Real Routes

Example:

  GET /products

Did it return:

  200 OK

?

Let’s test it.

Part 9 — Introducing SuperTest

Install:

SuperTest

  npm install -D supertest

Example:

  // index.js
  // Export the app object so that we can import and use it in our integration tests:
  const express = require('express');
  const app = express();

  // ...rest of the code

  module.exports = { app }
  // http.test.js
  import { test, expect } from 'vitest';
  import request from 'supertest';
  import { app } from "./index";

  // Test route:
  const response = await request(app).get( '/products');

  test("Response status", ()=>{
    expect(response.status).toBe(200);
  })

Test using npm run test.

Amazing.

No browser required.

Part 10 — Testing Protected Routes

Route:

  GET /admin

requires login.

Test:

  test("Protected Routes: Admin", async ()=>{
    const response = await request(app).get('/admin');
    // Check:
    expect(response.status).toBe(302);
  });

Meaning:

  Redirect to login

Now we know protection works.

Part 11 — Testing Error Cases

Most beginners test:

  Happy Path

only.

Professionals test:

  Failure Paths

first.

Example:

  GET /products/99999

Expected:

  expect(response.status).toBe(404);

Important.

Broken systems usually fail around edge cases.

Part 12 — Mocking

Sometimes dependencies are expensive.

Example:

  Database

  API

  Filesystem

Instead of:

  realDatabase

use:

  fakeDatabase

Example:

  import sqlite from 'node:sqlite';
  import path from 'node:path';

  const testDbPath = path.join(process.cwd(), 'test.sqlite');

  const testDb = new sqlite.DatabaseSync(testDbPath);

  testDb.exec(`
    CREATE TABLE IF NOT EXISTS products (
      id INTEGER PRIMARY KEY,
      name TEXT
    );

    DELETE FROM products;

    INSERT INTO products (name)
    VALUES ('Test Product');
  `);

  vi.mock('../db.js', () => ({
    default: testDb,
  }));

The vi.mock function replaces the real database with our test database during testing.

The syntax basically says: “When the code imports ../db.js, give it testDb instead of the real database.” thereby bypassing the real database and allowing us to run tests without affecting real data. We are mocking (i.e. faking) the database dependency’s default export.

Now tests run:

  Fast

  Predictably

Without touching real data.

Read more about mocking.

Part 13 — Test Isolation

Bad:

  Test A

  creates data

  Test B

  depends on it

Order matters.

Chaos follows.

Good:

  Each test independent

Every test should be able to run:

  Alone

  In Any Order

Critical principle.

Part 14 — Coverage

Question:

  How much code is tested?

Answer:

  Coverage

Install:

  npm i -D @vitest/coverage-v8

Run:

  npx vitest --coverage

This will produce a report showing how much of your code is covered by tests.

Example:

  Validation

  95%
  Permissions

  100%

Coverage helps.

But:

  100% coverage ≠ 100% correctness

A terrible test still counts.

Read more about coverage

Part 15 — Testing Strategy

Prioritize testing:

  Validation

  Authentication

  Authorization

  Business Logic

Lower priority:

  Simple Templates

  Static Pages

Rule:

Test things that can cause expensive mistakes.

Test things that change often.

Deleting products:

  High Value

Homepage title:

  Lower Value

Part 16 — Continuous Integration

Imagine:

  Developer pushes code

Automatically:

  Run Tests

If tests fail:

  Reject Deployment

This is called:

  Continuous Integration

or:

  CI

Used everywhere.

Examples:

Common Beginner Mistakes

Testing Only Happy Paths

Always test failures.

Using Production Databases

Never.

Use test databases.

Tests Depending on Order

Each test should be independent.

Massive Integration Tests

Keep tests focused.

Chasing Coverage Numbers

Quality matters more than percentages.

Bonus Challenge

Create a test suite for:

  Authentication

including:

  Valid Login

  Invalid Login

  Logout

  Session Creation

Goal:

  100% passing tests

for:

  Authentication

  Authorization

  Validation

These are the most security-sensitive parts of your CMS.

Key Takeaways

Today you learned:

A professional application is not defined by how many features it has. It is defined by how confidently those features can be changed without breaking existing behavior. Testing is the mechanism that creates that confidence.

At this stage, your CMS is no longer just functional and secure—it is becoming maintainable. That distinction becomes increasingly important as projects grow from hundreds of lines of code to tens of thousands.


⚠️ A large part of the content of this module was created using Generative AI (ChatGPT). The synthetic (AI-generated) content was reviewed and curated by Kostas Minaidis.


Project maintained by in-tech-gration Hosted on GitHub Pages — Theme by mattgraham