How to snapshot test in Node.js
Snapshot testing captures the output of functions or API responses and compares them to stored snapshots to detect unexpected changes. With over 12 years of Node.js development experience since 2014 and as the creator of CoreUI, I’ve used snapshot tests to catch regressions in APIs. Jest provides built-in snapshot testing that creates readable, version-controlled snapshots of complex data structures and responses. This approach catches unexpected changes in data formats, API responses, and object structures without manual assertions.
Use Jest snapshot testing to capture and compare complex data structures, preventing unintended changes.
Basic snapshot testing:
// userFormatter.js
function formatUser(user) {
return {
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
createdAt: new Date(user.createdAt).toISOString(),
isActive: user.status === 'active'
}
}
module.exports = { formatUser }
// userFormatter.test.js
const { formatUser } = require('./userFormatter')
describe('formatUser', () => {
it('formats user data correctly', () => {
const user = {
id: 1,
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
createdAt: '2024-01-15',
status: 'active'
}
const result = formatUser(user)
expect(result).toMatchSnapshot()
})
})
// Creates snapshot file:
// __snapshots__/userFormatter.test.js.snap
Snapshot of API responses:
// api.js
const express = require('express')
const app = express()
app.get('/api/users/:id', (req, res) => {
res.json({
id: parseInt(req.params.id),
name: 'John Doe',
email: '[email protected]',
profile: {
age: 30,
city: 'New York'
},
roles: ['user', 'admin'],
metadata: {
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z'
}
})
})
module.exports = app
// api.test.js
const request = require('supertest')
const app = require('./api')
describe('GET /api/users/:id', () => {
it('returns user data with correct structure', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200)
expect(response.body).toMatchSnapshot()
})
})
Snapshot with dynamic data:
// reportGenerator.js
function generateReport(data) {
return {
title: 'Monthly Report',
generatedAt: new Date().toISOString(),
data: {
totalUsers: data.users,
totalOrders: data.orders,
revenue: data.revenue
},
summary: `Generated report with ${data.users} users`
}
}
module.exports = { generateReport }
// reportGenerator.test.js
const { generateReport } = require('./reportGenerator')
describe('generateReport', () => {
it('generates report with correct structure', () => {
const mockData = {
users: 100,
orders: 250,
revenue: 5000
}
const result = generateReport(mockData)
// Exclude dynamic fields from snapshot
expect(result).toMatchSnapshot({
generatedAt: expect.any(String)
})
})
})
Property matchers:
// orderService.js
class OrderService {
createOrder(items) {
return {
id: Math.random().toString(36),
items: items,
total: items.reduce((sum, item) => sum + item.price, 0),
createdAt: new Date(),
status: 'pending'
}
}
}
module.exports = OrderService
// orderService.test.js
const OrderService = require('./orderService')
describe('OrderService', () => {
it('creates order with correct structure', () => {
const service = new OrderService()
const items = [
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 }
]
const result = service.createOrder(items)
expect(result).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date)
})
})
})
Inline snapshots:
// config.js
function loadConfig() {
return {
appName: 'MyApp',
version: '1.0.0',
features: {
auth: true,
analytics: true
},
endpoints: {
api: 'https://api.example.com',
cdn: 'https://cdn.example.com'
}
}
}
module.exports = { loadConfig }
// config.test.js
const { loadConfig } = require('./config')
describe('loadConfig', () => {
it('returns correct config', () => {
const config = loadConfig()
expect(config).toMatchInlineSnapshot(`
{
"appName": "MyApp",
"endpoints": {
"api": "https://api.example.com",
"cdn": "https://cdn.example.com",
},
"features": {
"analytics": true,
"auth": true,
},
"version": "1.0.0",
}
`)
})
})
Snapshot testing arrays:
// dataProcessor.js
function processUsers(users) {
return users
.filter(user => user.active)
.map(user => ({
id: user.id,
name: user.name,
email: user.email
}))
.sort((a, b) => a.id - b.id)
}
module.exports = { processUsers }
// dataProcessor.test.js
const { processUsers } = require('./dataProcessor')
describe('processUsers', () => {
it('processes user list correctly', () => {
const users = [
{ id: 2, name: 'Jane', email: '[email protected]', active: true },
{ id: 1, name: 'John', email: '[email protected]', active: true },
{ id: 3, name: 'Bob', email: '[email protected]', active: false }
]
const result = processUsers(users)
expect(result).toMatchSnapshot()
})
})
Custom serializers:
// dateUtils.js
function formatDate(date) {
return {
formatted: date.toISOString(),
timestamp: date.getTime(),
date: date
}
}
module.exports = { formatDate }
// dateUtils.test.js
const { formatDate } = require('./dateUtils')
expect.addSnapshotSerializer({
test: (val) => val instanceof Date,
print: (val) => `Date<${val.toISOString()}>`
})
describe('formatDate', () => {
it('formats date correctly', () => {
const date = new Date('2024-01-15T10:00:00Z')
const result = formatDate(date)
expect(result).toMatchSnapshot()
})
})
Updating snapshots:
# When intentional changes are made, update snapshots
npm test -- --updateSnapshot
# Or use interactive mode
npm test -- --watch
# Press 'u' to update snapshots
Snapshot testing error messages:
// validator.js
class ValidationError extends Error {
constructor(field, message) {
super(`Validation failed for ${field}: ${message}`)
this.field = field
this.code = 'VALIDATION_ERROR'
}
}
function validateUser(user) {
if (!user.email) {
throw new ValidationError('email', 'Email is required')
}
if (!user.name) {
throw new ValidationError('name', 'Name is required')
}
}
module.exports = { validateUser, ValidationError }
// validator.test.js
const { validateUser } = require('./validator')
describe('validateUser', () => {
it('throws validation error for missing email', () => {
expect(() => {
validateUser({ name: 'John' })
}).toThrowErrorMatchingSnapshot()
})
it('throws validation error for missing name', () => {
expect(() => {
validateUser({ email: '[email protected]' })
}).toThrowErrorMatchingSnapshot()
})
})
Best practices:
describe('Snapshot best practices', () => {
it('keeps snapshots small and focused', () => {
const data = {
id: 1,
name: 'Test',
// Include only relevant data
relevantField: 'value'
}
expect(data).toMatchSnapshot()
})
it('uses property matchers for dynamic values', () => {
const result = {
id: Math.random(),
timestamp: Date.now(),
value: 'static'
}
expect(result).toMatchSnapshot({
id: expect.any(Number),
timestamp: expect.any(Number)
})
})
it('groups related snapshots', () => {
const user1 = { id: 1, name: 'User 1' }
const user2 = { id: 2, name: 'User 2' }
expect({ user1, user2 }).toMatchSnapshot()
})
})
Best Practice Note
Snapshot tests are excellent for detecting unintended changes in data structures and API responses. Keep snapshots small and focused—large snapshots are hard to review. Use property matchers for dynamic values like timestamps and IDs. Review snapshot changes carefully during code review—they’re version controlled. Update snapshots when changes are intentional using --updateSnapshot. Combine with traditional assertions for important invariants. This is how we use snapshot testing in CoreUI Node.js APIs—catching regressions in response formats, preventing breaking changes, and maintaining consistent data structures across versions.



