Table of Contents#
- Prerequisites
- Understanding the Problem: Repetitive Headers in Tests
- Solution: Setting Default Headers in Supertest
- Step-by-Step Implementation
- Advanced Use Cases
- Best Practices
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have the following:
- Node.js (v14+ recommended) and npm installed.
- A basic Express.js API (we’ll use a sample API for demonstration).
- Familiarity with Mocha (test runner) and Chai (assertion library).
- Supertest installed (we’ll add it via npm).
If you’re starting from scratch, create a new project and install dependencies:
mkdir supertest-default-headers && cd supertest-default-headers
npm init -y
npm install express --save
npm install mocha supertest chai dotenv --save-devUnderstanding the Problem: Repetitive Headers in Tests#
Most APIs require consistent headers for authentication (e.g., Authorization: Bearer <token>), content type (e.g., Content-Type: application/json), or custom headers (e.g., X-API-Version: 1). Without default headers, you’ll repeat these set() calls in every test case.
Consider a sample API with two endpoints:
GET /api/users: Requires anAuthorizationheader.POST /api/users: RequiresAuthorizationandContent-Type: application/json.
Here’s what tests might look like without default headers:
// tests/users.test.js
const request = require('supertest');
const { expect } = require('chai');
const app = require('../app');
describe('User API', () => {
const authToken = 'static-test-token'; // Hardcoded for example
it('GET /api/users should return a list of users (with auth)', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`); // Repeated header
expect(res.status).to.equal(200);
expect(res.body).to.be.an('array');
});
it('POST /api/users should create a new user (with auth and JSON)', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`) // Repeated
.set('Content-Type', 'application/json') // Repeated
.send({ name: 'John Doe' });
expect(res.status).to.equal(201);
expect(res.body.name).to.equal('John Doe');
});
});Notice the repeated .set('Authorization', ...) and .set('Content-Type', ...) calls. As your test suite grows (e.g., 50+ tests), this repetition becomes unmanageable.
Solution: Setting Default Headers in Supertest#
Supertest allows you to create a reusable request object with pre-configured headers. By wrapping Supertest in a helper function, you can define default headers once and reuse them across tests. This eliminates repetition and keeps tests DRY (Don’t Repeat Yourself).
Step-by-Step Implementation#
Let’s implement default headers for our sample API. We’ll start with a basic Express app, then refactor tests to use default headers.
Step 1: Project Setup#
First, create a simple Express API and a .env file for environment variables:
app.js (Sample Express API)#
// app.js
require('dotenv').config();
const express = require('express');
const app = express();
// Middleware to parse JSON bodies
app.use(express.json());
// Mock auth middleware (checks for Authorization header)
const requireAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// Sample endpoints
app.get('/api/users', requireAuth, (req, res) => {
res.json([{ id: 1, name: 'Test User' }]);
});
app.post('/api/users', requireAuth, (req, res) => {
res.status(201).json({ id: 2, name: req.body.name });
});
// Start server (only if run directly, not in tests)
if (require.main === module) {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
}
module.exports = app; // Export for testing.env File#
TEST_AUTH_TOKEN=static-test-token # Replace with a real token in productionStep 2: Write Initial Tests (with Repetition)#
Create a test file with repetitive headers to demonstrate the problem:
// tests/users.test.js
const request = require('supertest');
const { expect } = require('chai');
const app = require('../app');
require('dotenv').config();
describe('User API Endpoints', () => {
const authToken = process.env.TEST_AUTH_TOKEN;
// Test 1: GET /api/users (requires Authorization)
it('should return a list of users when authenticated', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`); // Repeated
expect(res.status).to.equal(200);
expect(res.body).to.be.an('array');
});
// Test 2: POST /api/users (requires Authorization + Content-Type)
it('should create a new user when authenticated and JSON is sent', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`) // Repeated
.set('Content-Type', 'application/json') // Repeated
.send({ name: 'New User' });
expect(res.status).to.equal(201);
expect(res.body.name).to.equal('New User');
});
// Test 3: Unauthorized access (missing Authorization)
it('should return 401 when Authorization header is missing', async () => {
const res = await request(app).get('/api/users'); // No headers
expect(res.status).to.equal(401);
});
});Run the tests with npx mocha tests/**/*.test.js—they’ll pass, but the repetition is clear.
Step 3: Refactor with Default Headers#
To eliminate repetition, create a helper function that initializes Supertest with default headers. This function will wrap supertest(app) and pre-set headers like Authorization and Content-Type.
Create a Test Helper#
Create tests/helpers/requestWithDefaults.js to centralize default headers:
// tests/helpers/requestWithDefaults.js
const request = require('supertest');
const app = require('../../app');
require('dotenv').config();
/**
* Creates a Supertest request with default headers.
* @param {Object} customHeaders - Optional headers to override defaults.
* @returns {Supertest.Request} - Supertest request object with headers.
*/
const requestWithDefaults = (customHeaders = {}) => {
// Define default headers
const defaultHeaders = {
'Content-Type': 'application/json', // Required for JSON bodies
'Authorization': `Bearer ${process.env.TEST_AUTH_TOKEN}`, // Auth token from .env
...customHeaders, // Allow overriding defaults with custom headers
};
// Return Supertest request with headers set
return request(app).set(defaultHeaders);
};
module.exports = requestWithDefaults;Refactor Tests to Use the Helper#
Update tests/users.test.js to use requestWithDefaults instead of raw supertest(app):
// tests/users.test.js
const requestWithDefaults = require('./helpers/requestWithDefaults');
const { expect } = require('chai');
describe('User API Endpoints', () => {
// Test 1: GET /api/users (uses default Authorization)
it('should return a list of users when authenticated', async () => {
const res = await requestWithDefaults().get('/api/users');
// No need for .set('Authorization')—defaults handle it!
expect(res.status).to.equal(200);
expect(res.body).to.be.an('array');
});
// Test 2: POST /api/users (uses default Authorization + Content-Type)
it('should create a new user when authenticated and JSON is sent', async () => {
const res = await requestWithDefaults()
.post('/api/users')
.send({ name: 'New User' });
// No need for .set('Content-Type') or .set('Authorization')!
expect(res.status).to.equal(201);
expect(res.body.name).to.equal('New User');
});
// Test 3: Unauthorized access (override default Authorization)
it('should return 401 when Authorization header is invalid', async () => {
const res = await requestWithDefaults({
Authorization: 'Bearer invalid-token', // Override default
}).get('/api/users');
expect(res.status).to.equal(401);
});
});Result: Tests now run without repetitive .set() calls! The helper function encapsulates default headers, making tests cleaner and easier to update.
Advanced Use Cases#
Dynamic Headers (e.g., Expiring JWT Tokens)#
Static tokens (like TEST_AUTH_TOKEN) work for simple cases, but real-world APIs often use short-lived JWT tokens. To handle dynamic tokens, fetch the token once before tests run (e.g., via a login endpoint) and inject it into default headers.
Update the helper to fetch a token dynamically:
// tests/helpers/requestWithDefaults.js
const request = require('supertest');
const app = require('../../app');
let authToken; // Cache token for reuse
// Fetch token before all tests run
before(async () => {
const loginRes = await request(app)
.post('/api/login') // Assume your API has a login endpoint
.send({ email: '[email protected]', password: 'test-password' });
authToken = loginRes.body.token; // Extract token from response
});
const requestWithDefaults = (customHeaders = {}) => {
const defaultHeaders = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`, // Use dynamic token
...customHeaders,
};
return request(app).set(defaultHeaders);
};
module.exports = requestWithDefaults;Note: Use Mocha’s before() hook to run the login once before all tests, or beforeEach() to refresh the token before each test (useful for short-lived tokens).
Overriding Default Headers#
You may need to override defaults for specific tests (e.g., testing unauthorized access or different user roles). As shown earlier, pass an object to requestWithDefaults to override headers:
// Test with no auth (override Authorization with undefined)
it('should return 401 when no Authorization header is sent', async () => {
const res = await requestWithDefaults({ Authorization: undefined })
.get('/api/users');
expect(res.status).to.equal(401);
});
// Test with a different Content-Type (e.g., form data)
it('should reject non-JSON bodies', async () => {
const res = await requestWithDefaults({ 'Content-Type': 'application/x-www-form-urlencoded' })
.post('/api/users')
.send('name=New User'); // Form data instead of JSON
expect(res.status).to.equal(400); // Assume API rejects non-JSON
});Multiple Default Header Sets#
For complex apps (e.g., admin vs. regular user tests), create multiple helper functions for different header sets:
// tests/helpers/requestWithDefaults.js
// Regular user headers
const requestWithUserDefaults = (customHeaders = {}) => {
return request(app).set({
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`,
...customHeaders,
});
};
// Admin user headers
const requestWithAdminDefaults = (customHeaders = {}) => {
return request(app).set({
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`,
'X-Admin-Role': 'true', // Custom admin header
...customHeaders,
});
};
module.exports = { requestWithUserDefaults, requestWithAdminDefaults };Use them in tests:
const { requestWithUserDefaults, requestWithAdminDefaults } = require('./helpers/requestWithDefaults');
it('should allow admins to delete users', async () => {
const res = await requestWithAdminDefaults().delete('/api/users/1');
expect(res.status).to.equal(204);
});
it('should deny regular users from deleting users', async () => {
const res = await requestWithUserDefaults().delete('/api/users/1');
expect(res.status).to.equal(403);
});Best Practices#
- Centralize Helpers: Store default header logic in a dedicated helper file (e.g.,
tests/helpers/requestWithDefaults.js) for reusability. - Use Environment Variables: Never hard-code tokens or secrets. Use
dotenvto load sensitive data from.envfiles (add.envto.gitignore!). - Document Defaults: Add comments to your helper function explaining which headers are set by default (e.g.,
// Default: Content-Type: application/json, Authorization: Bearer <token>). - Test Edge Cases: Explicitly test scenarios where headers are missing, invalid, or overridden to ensure your API behaves as expected.
- Refresh Dynamic Tokens: For short-lived tokens (e.g., JWT), use
beforeEach()instead ofbefore()to refresh tokens before each test and avoid 401 errors.
Conclusion#
Setting default headers in Supertest is a simple yet powerful way to reduce repetition, improve maintainability, and make your Node.js API tests cleaner. By encapsulating headers in a helper function, you eliminate redundant code and ensure consistency across your test suite. Whether you’re using static tokens, dynamic JWTs, or multiple user roles, this approach scales to fit your needs.
Start implementing default headers today, and watch your test suite become more efficient and less error-prone!