Backend testing is crucial for ensuring the reliability and functionality of server-side applications. In this module, you'll learn how to test APIs, database interactions, and server endpoints using modern testing tools and best practices.
Master the art of testing RESTful APIs, including request/response handling, status codes, and data validation.
Learn how to test database operations, including CRUD operations, data integrity, and transaction handling.
Understand how to write and maintain integration tests that verify the interaction between different components.
API endpoint testing focuses on ensuring that your REST endpoints properly handle requests and return the expected responses. For this, we can use tools like Supertest along with Jest.
const request = require('supertest'); const server = require('../api/server'); describe('server.js', () => { describe('GET /', () => { it('returns 200 OK', async () => { const response = await request(server).get('/'); expect(response.status).toBe(200); }); it('returns JSON', async () => { const response = await request(server).get('/'); expect(response.type).toBe('application/json'); }); it('returns the expected message', async () => { const response = await request(server).get('/'); expect(response.body.message).toBe('API is running'); }); }); });
Authentication is a critical part of most applications, and it's essential to test these endpoints thoroughly.
describe('Authentication Endpoints', () => { beforeAll(async () => { // Clear test database before all tests await db('users').truncate(); }); describe('POST /api/auth/register', () => { it('returns 201 Created when registration succeeds', async () => { const newUser = { username: 'testuser', password: 'password123' }; const response = await request(server) .post('/api/auth/register') .send(newUser); expect(response.status).toBe(201); }); it('saves the user to the database', async () => { const users = await db('users'); expect(users).toHaveLength(1); }); it('returns 400 Bad Request if username is missing', async () => { const invalidUser = { password: 'password123' }; const response = await request(server) .post('/api/auth/register') .send(invalidUser); expect(response.status).toBe(400); }); }); describe('POST /api/auth/login', () => { it('returns 200 OK and token when login succeeds', async () => { const credentials = { username: 'testuser', password: 'password123' }; const response = await request(server) .post('/api/auth/login') .send(credentials); expect(response.status).toBe(200); expect(response.body.token).toBeDefined(); }); it('returns 401 Unauthorized with invalid credentials', async () => { const invalidCreds = { username: 'testuser', password: 'wrongpassword' }; const response = await request(server) .post('/api/auth/login') .send(invalidCreds); expect(response.status).toBe(401); }); }); });
Protected routes require a valid token for access. Testing these routes involves first obtaining a token and then using it in subsequent requests.
describe('Protected Routes', () => { let token; beforeAll(async () => { // Clear test database await db('users').truncate(); // Register a test user await request(server) .post('/api/auth/register') .send({ username: 'protecteduser', password: 'password123' }); // Login to get a token const response = await request(server) .post('/api/auth/login') .send({ username: 'protecteduser', password: 'password123' }); token = response.body.token; }); describe('GET /api/resources', () => { it('returns 401 without token', async () => { const response = await request(server).get('/api/resources'); expect(response.status).toBe(401); }); it('returns 200 with valid token', async () => { const response = await request(server) .get('/api/resources') .set('Authorization', `Bearer ${token}`); expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); }); }); });
Testing database operations is crucial for ensuring your data layer works correctly. This typically involves testing your models and data access functions.
It's important to use a separate database for testing to avoid affecting production data. A common practice is to use SQLite in-memory database for testing:
// knexfile.js module.exports = { development: { client: 'sqlite3', connection: { filename: './data/dev.sqlite3' }, // ... other config }, testing: { client: 'sqlite3', connection: { filename: ':memory:' // In-memory SQLite database }, // ... other config seeds: { directory: './data/seeds' } } };
// dbTestHelpers.js const db = require('../data/dbConfig'); async function truncateAllTables() { // Truncate all tables to start with fresh data await db('users').truncate(); await db('posts').truncate(); await db('comments').truncate(); } async function seedTestData() { // Insert test data await db('users').insert([ { id: 1, username: 'testuser1', password: 'hashed_password' }, { id: 2, username: 'testuser2', password: 'hashed_password' } ]); // ... other seed data } module.exports = { truncateAllTables, seedTestData };
For a robust application, each Create, Read, Update, and Delete operation should be thoroughly tested.
const db = require('../data/dbConfig'); const Users = require('../models/users-model'); const { truncateAllTables } = require('./dbTestHelpers'); describe('Users Model', () => { beforeEach(async () => { await truncateAllTables(); }); describe('getAll()', () => { it('returns an empty array if no users exist', async () => { const users = await Users.getAll(); expect(users).toEqual([]); }); it('returns all users in the database', async () => { // Add test users await db('users').insert([ { username: 'user1', password: 'pass1' }, { username: 'user2', password: 'pass2' } ]); const users = await Users.getAll(); expect(users).toHaveLength(2); expect(users[0].username).toBe('user1'); expect(users[1].username).toBe('user2'); }); }); describe('findById()', () => { it('returns the user with the specified id', async () => { // Add a test user const [id] = await db('users').insert({ username: 'finduser', password: 'pass' }); const user = await Users.findById(id); expect(user.username).toBe('finduser'); }); it('returns undefined for non-existent id', async () => { const user = await Users.findById(9999); expect(user).toBeUndefined(); }); }); describe('add()', () => { it('adds the user to the database', async () => { const newUser = { username: 'newuser', password: 'newpass' }; await Users.add(newUser); const users = await db('users'); expect(users).toHaveLength(1); expect(users[0].username).toBe('newuser'); }); it('returns the added user with an id', async () => { const newUser = { username: 'adduser', password: 'addpass' }; const added = await Users.add(newUser); expect(added.id).toBeDefined(); expect(added.username).toBe('adduser'); }); }); // ... tests for update() and remove() });
Some database operations involve multiple steps or transactions that should be tested together.
describe('Posts Model - Complex Operations', () => { beforeEach(async () => { await truncateAllTables(); await seedTestData(); }); describe('createPostWithTags()', () => { it('creates a post with multiple tags', async () => { const newPost = { title: 'Test Post', content: 'This is a test post', user_id: 1, tags: ['javascript', 'testing', 'jest'] }; const result = await Posts.createPostWithTags(newPost); // Check post was created expect(result.post.id).toBeDefined(); expect(result.post.title).toBe('Test Post'); // Check tags were created expect(result.tags).toHaveLength(3); // Check post-tag relationships const postTags = await db('post_tags') .where({ post_id: result.post.id }); expect(postTags).toHaveLength(3); }); it('rolls back if tag creation fails', async () => { // Mock db.transaction to simulate failure const originalTransaction = db.transaction; db.transaction = jest.fn(async (callback) => { const trx = { commit: jest.fn(), rollback: jest.fn(), insert: jest.fn(() => { throw new Error('Test error'); }), // ... other trx methods as needed }; try { await callback(trx); } catch (err) { await trx.rollback(); throw err; } }); const newPost = { title: 'Rollback Test Post', content: 'This should be rolled back', user_id: 1, tags: ['javascript', 'testing'] }; await expect(Posts.createPostWithTags(newPost)) .rejects.toThrow('Test error'); // Check nothing was added const posts = await db('posts'); expect(posts).toHaveLength(0); // Restore original transaction function db.transaction = originalTransaction; }); }); });
Test-Driven Development (TDD) is an effective approach for developing robust backend APIs. The process involves:
// Step 1: Write a failing test describe('POST /api/resources', () => { it('creates a new resource with valid data', async () => { const newResource = { name: 'Test Resource', value: 100 }; const response = await request(server) .post('/api/resources') .send(newResource); expect(response.status).toBe(201); expect(response.body.id).toBeDefined(); expect(response.body.name).toBe('Test Resource'); // Verify it was saved to the database const resources = await db('resources'); expect(resources).toHaveLength(1); }); });
Jest provides built-in code coverage reporting. You can enable it by adding the --coverage
flag to your test command:
// package.json { "scripts": { "test": "jest --watch", "test:coverage": "jest --coverage" } }
// Well-organized test structure describe('Users API', () => { describe('GET /api/users', () => { // Tests for listing users }); describe('GET /api/users/:id', () => { // Tests for retrieving a specific user }); describe('POST /api/users', () => { // Tests for creating users }); describe('PUT /api/users/:id', () => { // Tests for updating users }); describe('DELETE /api/users/:id', () => { // Tests for deleting users }); });
Setting up the right testing environment is essential for reliable and efficient tests.
NODE_ENV=test
)// jest.setup.js const db = require('./data/dbConfig'); // Global setup before all tests beforeAll(async () => { // Run migrations to set up the test database schema await db.migrate.latest(); }); // Global cleanup after all tests afterAll(async () => { // Close database connection await db.destroy(); }); // Reset database before each test suite beforeEach(async () => { // Run seeds to reset to a known state await db.seed.run(); });
// jest.config.js module.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['./jest.setup.js'], collectCoverageFrom: [ '**/*.{js,jsx}', '!**/node_modules/**', '!**/vendor/**', '!**/*.config.js', ], testPathIgnorePatterns: ['/node_modules/'], verbose: true };
Build a comprehensive test suite for a Node.js backend application
View Project