Module 4: Backend Testing
Learning Objectives
By the end of this module, you should be able to:
- Implement comprehensive testing for backend applications
- Create tests for API endpoints using supertest
- Test database interactions with proper isolation
- Implement integration tests for complete workflows
- Structure your tests for maintainability and effectiveness
Video Lesson
Introduction to Backend Testing
Backend testing is crucial for ensuring your API endpoints, database interactions, and business logic work as expected. It builds upon unit testing concepts, but focuses on testing the server-side components of your application.
Types of Backend Tests
- API Testing: Testing API endpoints to ensure they return the correct responses
- Database Testing: Testing interactions with your database
- Integration Testing: Testing how multiple components work together
- End-to-End Testing: Testing complete workflows from start to finish
Backend testing helps ensure your application's core functionality works correctly before deploying to production.
Testing API Endpoints with Supertest
Supertest is a popular library for testing HTTP servers in Node.js. It allows you to make HTTP requests to your application and assert the responses.
Setting Up Supertest
npm install --save-dev supertest
Basic API Test Example
// user.test.js
const request = require('supertest');
const app = require('../app'); // Your Express app
describe('User API', () => {
test('GET /api/users returns all users', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(Array.isArray(response.body.users)).toBe(true);
});
test('GET /api/users/:id returns a specific user', async () => {
const response = await request(app).get('/api/users/1');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id', 1);
expect(response.body).toHaveProperty('username');
});
test('POST /api/users creates a new user', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(userData);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.username).toBe('testuser');
expect(response.body.email).toBe('test@example.com');
// Password should not be returned
expect(response.body).not.toHaveProperty('password');
});
});
Testing Authentication Endpoints
Testing authentication endpoints requires special consideration for handling credentials and tokens.
Testing Registration and Login
describe('Authentication API', () => {
test('POST /api/auth/register creates a new user', async () => {
const userData = {
username: 'newuser',
email: 'newuser@example.com',
password: 'securepassword123'
};
const response = await request(app)
.post('/api/auth/register')
.send(userData);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('message', 'User registered successfully');
expect(response.body).toHaveProperty('user');
expect(response.body.user).toHaveProperty('id');
expect(response.body.user.username).toBe('newuser');
});
test('POST /api/auth/login authenticates a user', async () => {
const credentials = {
email: 'newuser@example.com',
password: 'securepassword123'
};
const response = await request(app)
.post('/api/auth/login')
.send(credentials);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('message', 'Login successful');
expect(response.body).toHaveProperty('token');
});
});
Testing Protected Routes
describe('Protected Routes', () => {
let authToken;
beforeAll(async () => {
// Login to get a token
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'newuser@example.com',
password: 'securepassword123'
});
authToken = response.body.token;
});
test('GET /api/profile returns user profile when authenticated', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'newuser');
});
test('GET /api/profile returns 401 when not authenticated', async () => {
const response = await request(app)
.get('/api/profile');
expect(response.status).toBe(401);
});
});
Testing Database Interactions
When testing database interactions, it's important to use a test database or mock the database to avoid affecting production data.
Approaches to Database Testing
- Test Database: Use a separate database for testing
- In-Memory Database: Use an in-memory database for faster tests
- Mocking: Mock database calls to avoid database interaction
Setting Up a Test Database
// In your test setup file
beforeAll(async () => {
// Connect to test database
await mongoose.connect(process.env.TEST_DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
afterAll(async () => {
// Disconnect after tests
await mongoose.connection.close();
});
// Clear database between tests
beforeEach(async () => {
await mongoose.connection.dropDatabase();
});
Testing Database Models
describe('User Model', () => {
test('create & save user successfully', async () => {
const userData = {
username: 'testuser',
email: 'test@test.com',
password: 'Password123'
};
const validUser = new User(userData);
const savedUser = await validUser.save();
// Object Id should be defined when successfully saved to MongoDB
expect(savedUser._id).toBeDefined();
expect(savedUser.username).toBe(userData.username);
expect(savedUser.email).toBe(userData.email);
// Password should be hashed, not plain text
expect(savedUser.password).not.toBe(userData.password);
});
test('create user with missing required field fails', async () => {
const userWithoutRequiredField = new User({ username: 'test' });
let err;
try {
await userWithoutRequiredField.save();
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
});
});
Integration Testing
Integration tests verify that different parts of your application work together correctly. These tests are valuable for catching issues that might not be apparent when testing components in isolation.
Example: User Registration and Authentication Flow
describe('User Registration and Authentication Flow', () => {
const userData = {
username: 'integrationuser',
email: 'integration@example.com',
password: 'SecurePass123'
};
let userId;
let authToken;
test('register a new user', async () => {
const response = await request(app)
.post('/api/auth/register')
.send(userData);
expect(response.status).toBe(201);
expect(response.body.user).toHaveProperty('id');
userId = response.body.user.id;
});
test('login with the new user', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: userData.email,
password: userData.password
});
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
authToken = response.body.token;
});
test('access protected route with auth token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.username).toBe(userData.username);
});
test('update user profile', async () => {
const updateData = {
bio: 'This is a test user'
};
const response = await request(app)
.put(`/api/users/${userId}`)
.set('Authorization', `Bearer ${authToken}`)
.send(updateData);
expect(response.status).toBe(200);
expect(response.body.bio).toBe(updateData.bio);
});
test('logout and token should be invalid', async () => {
// Logout
await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${authToken}`);
// Try to access protected route
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(401);
});
});
Advanced Testing Topics
Testing Error Handling
describe('Error Handling', () => {
test('returns 404 for non-existent routes', async () => {
const response = await request(app).get('/non-existent-route');
expect(response.status).toBe(404);
});
test('returns 400 for bad request format', async () => {
const response = await request(app)
.post('/api/users')
.send({ invalidField: 'data' });
expect(response.status).toBe(400);
});
test('handles server errors gracefully', async () => {
// Mock a function to throw an error
jest.spyOn(User, 'findById').mockImplementationOnce(() => {
throw new Error('Database connection failed');
});
const response = await request(app).get('/api/users/1');
expect(response.status).toBe(500);
expect(response.body).toHaveProperty('message');
});
});
Testing Middleware
describe('Authentication Middleware', () => {
test('authenticateToken middleware blocks requests without token', async () => {
const req = { headers: {} };
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
const next = jest.fn();
const authMiddleware = require('../middleware/auth');
authMiddleware.authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
message: expect.any(String)
}));
expect(next).not.toHaveBeenCalled();
});
test('authenticateToken middleware allows requests with valid token', async () => {
// Create a valid token
const jwt = require('jsonwebtoken');
const token = jwt.sign({ id: 1 }, process.env.JWT_SECRET);
const req = {
headers: {
authorization: `Bearer ${token}`
}
};
const res = {};
const next = jest.fn();
const authMiddleware = require('../middleware/auth');
authMiddleware.authenticateToken(req, res, next);
expect(next).toHaveBeenCalled();
expect(req.user).toEqual(expect.objectContaining({ id: 1 }));
});
});
Additional Resources
Practice Assignment
Now it's time to practice testing backend applications! Create comprehensive tests for your authentication system:
- Write API tests for registration, login, and profile endpoints
- Test protected routes with and without authentication
- Create database tests for your user model
- Implement integration tests for the complete authentication flow
- Test error handling for various scenarios
Sprint Challenge Preparation
Now that you've completed all four modules in this sprint, it's time to prepare for the sprint challenge. Make sure you understand:
- Authentication concepts and implementation
- JWT-based token authentication
- Writing effective tests for both unit and API functionality
- Testing database interactions and protected routes