Part 13 – Comprehensive Testing Strategies

Robust testing is essential for maintaining reliable Node.js applications. This guide covers unit, integration, and end-to-end testing with modern JavaScript testing tools.

1. Testing Pyramid Overview

Unit Tests

  • Test individual functions/components
  • Fast execution
  • High isolation (mocks/stubs)
  • ~70% of test coverage

Integration Tests

  • Test component interactions
  • Include some external services
  • ~20% of test coverage

E2E Tests

  • Test complete workflows
  • Slowest but most realistic
  • ~10% of test coverage

2. Testing Setup with Jest

Basic Configuration

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/tests/"
    ],
    "setupFilesAfterEnv": ["./tests/setup.js"]
  }
}

// Install Jest
npm install --save-dev jest @types/jest

Basic Test Example

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

// tests/calculator.test.js
const { add } = require('../utils/calculator');

describe('Calculator', () => {
    describe('add()', () => {
        it('should add two numbers correctly', () => {
            expect(add(2, 3)).toBe(5);
        });

        it('should handle negative numbers', () => {
            expect(add(-1, -1)).toBe(-2);
        });
    });
});

3. Testing Asynchronous Code

Callbacks

test('fetchData with callback', done => {
    fetchData((err, data) => {
        expect(err).toBeNull();
        expect(data).toEqual({ id: 1 });
        done(); // Test completes
    });
});

Promises

test('fetchData with promises', () => {
    return fetchData().then(data => {
        expect(data).toEqual({ id: 1 });
    });
});

// OR with async/await
test('fetchData async/await', async () => {
    const data = await fetchData();
    expect(data).toEqual({ id: 1 });
});

4. Mocking Dependencies

Manual Mocks

// __mocks__/fs.js
module.exports = {
    readFile: jest.fn((path, cb) => 
        cb(null, 'mock file content'))
};

// In test file
jest.mock('fs');

test('reads file content', () => {
    const fs = require('fs');
    fs.readFile.mockImplementation((path, cb) => 
        cb(null, 'custom content'));

    readFile('test.txt', (err, data) => {
        expect(data).toBe('custom content');
    });
});

Jest Automatic Mocks

jest.mock('../services/userService');

const userService = require('../services/userService');
userService.getUser.mockResolvedValue({ id: 1 });

test('gets user', async () => {
    const user = await getUser(1);
    expect(userService.getUser).toHaveBeenCalledWith(1);
    expect(user).toEqual({ id: 1 });
});

5. Integration Testing

API Testing with Supertest

const request = require('supertest');
const app = require('../app');

describe('GET /api/users', () => {
    it('should return user list', async () => {
        const res = await request(app)
            .get('/api/users')
            .expect(200);

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

    it('should 404 for invalid route', async () => {
        await request(app)
            .get('/api/nonexistent')
            .expect(404);
    });
});

Database Testing

const mongoose = require('mongoose');
const User = require('../models/User');

beforeAll(async () => {
    await mongoose.connect(process.env.TEST_DB_URI);
});

afterEach(async () => {
    await User.deleteMany();
});

afterAll(async () => {
    await mongoose.disconnect();
});

test('creates user', async () => {
    const user = await User.create({
        name: 'Test',
        email: 'test@test.com'
    });

    expect(user._id).toBeDefined();
    expect(user.name).toBe('Test');
});

6. End-to-End Testing

Puppeteer Example

const puppeteer = require('puppeteer');

describe('Frontend E2E', () => {
    let browser, page;

    beforeAll(async () => {
        browser = await puppeteer.launch();
        page = await browser.newPage();
    });

    afterAll(async () => {
        await browser.close();
    });

    test('homepage loads', async () => {
        await page.goto('http://localhost:3000');
        await page.waitForSelector('#app');

        const title = await page.title();
        expect(title).toBe('My App');

        const heading = await page.$eval('h1', el => el.textContent);
        expect(heading).toContain('Welcome');
    });
});

7. Advanced Testing Techniques

Snapshot Testing

test('renders component correctly', () => {
    const component = renderComponent({ name: 'Test' });
    expect(component).toMatchSnapshot();
});

Property-Based Testing

const fc = require('fast-check');

test('addition is commutative', () => {
    fc.assert(
        fc.property(fc.integer(), fc.integer(), (a, b) => {
            return add(a, b) === add(b, a);
        })
    );
});

Next: Deploying Node.js Applications →

Testing Best Practices

  • Test behavior, not implementation
  • Keep tests isolated and independent
  • Use descriptive test names
  • Run tests in CI/CD pipeline
  • Aim for meaningful coverage (not just 100%)
  • Tag slow tests as such (jest --runInBand)

Leave a Comment

Your email address will not be published. Required fields are marked *