Testing Functions
Now that you know Jest's matchers, let's learn how to test functions thoroughly. We'll cover different function types, edge cases, and strategies for comprehensive testing.
Testing Pure Functions
Pure functions are the easiest to test - same input always gives same output:
// Function to test
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
// Tests
describe('capitalize', () => {
it('capitalizes a lowercase word', () => {
expect(capitalize('hello')).toBe('Hello');
});
it('handles already capitalized words', () => {
expect(capitalize('Hello')).toBe('Hello');
});
it('handles all uppercase words', () => {
expect(capitalize('HELLO')).toBe('Hello');
});
it('returns empty string for empty input', () => {
expect(capitalize('')).toBe('');
});
it('handles null/undefined', () => {
expect(capitalize(null)).toBe('');
expect(capitalize(undefined)).toBe('');
});
});
Testing Edge Cases
Always test the boundaries and unusual inputs:
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
describe('clamp', () => {
// Normal cases
it('returns value when within range', () => {
expect(clamp(5, 0, 10)).toBe(5);
});
// Boundary cases
it('returns min when value is below range', () => {
expect(clamp(-5, 0, 10)).toBe(0);
});
it('returns max when value is above range', () => {
expect(clamp(15, 0, 10)).toBe(10);
});
it('returns value when exactly at min', () => {
expect(clamp(0, 0, 10)).toBe(0);
});
it('returns value when exactly at max', () => {
expect(clamp(10, 0, 10)).toBe(10);
});
// Edge cases
it('handles when min equals max', () => {
expect(clamp(5, 5, 5)).toBe(5);
});
it('handles negative ranges', () => {
expect(clamp(-5, -10, -1)).toBe(-5);
});
});
Testing Functions with Multiple Return Types
Some functions return different types based on input:
function parseNumber(value) {
if (value === null || value === undefined) {
return null;
}
const num = Number(value);
if (isNaN(num)) {
return { error: 'Invalid number' };
}
return num;
}
describe('parseNumber', () => {
it('parses valid integer strings', () => {
expect(parseNumber('42')).toBe(42);
});
it('parses valid float strings', () => {
expect(parseNumber('3.14')).toBeCloseTo(3.14);
});
it('returns null for null input', () => {
expect(parseNumber(null)).toBeNull();
});
it('returns null for undefined input', () => {
expect(parseNumber(undefined)).toBeNull();
});
it('returns error object for invalid strings', () => {
expect(parseNumber('abc')).toEqual({ error: 'Invalid number' });
});
it('handles numbers directly', () => {
expect(parseNumber(42)).toBe(42);
});
});
Testing Functions That Throw
Test both the happy path and error conditions:
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
describe('divide', () => {
// Happy path
it('divides two positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('handles negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
});
it('handles decimal results', () => {
expect(divide(7, 2)).toBe(3.5);
});
// Error conditions
it('throws TypeError for non-number first argument', () => {
expect(() => divide('10', 2)).toThrow(TypeError);
expect(() => divide('10', 2)).toThrow('Arguments must be numbers');
});
it('throws TypeError for non-number second argument', () => {
expect(() => divide(10, '2')).toThrow(TypeError);
});
it('throws Error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
});
Testing Functions with Callbacks
For callback-based functions, use Jest's done callback:
function fetchData(callback) {
setTimeout(() => {
callback({ data: 'result' });
}, 100);
}
describe('fetchData', () => {
it('calls callback with data', (done) => {
fetchData((result) => {
expect(result).toEqual({ data: 'result' });
done(); // Signal that async test is complete
});
});
});
Or use promises for cleaner tests:
function fetchDataPromise(callback) {
return new Promise((resolve) => {
setTimeout(() => {
const result = { data: 'result' };
if (callback) callback(result);
resolve(result);
}, 100);
});
}
describe('fetchDataPromise', () => {
it('resolves with data', async () => {
const result = await fetchDataPromise();
expect(result).toEqual({ data: 'result' });
});
});
Testing Higher-Order Functions
Higher-order functions take or return functions:
function createMultiplier(factor) {
return (number) => number * factor;
}
describe('createMultiplier', () => {
it('returns a function', () => {
const double = createMultiplier(2);
expect(typeof double).toBe('function');
});
it('creates a working multiplier', () => {
const double = createMultiplier(2);
expect(double(5)).toBe(10);
});
it('works with different factors', () => {
const triple = createMultiplier(3);
const half = createMultiplier(0.5);
expect(triple(10)).toBe(30);
expect(half(10)).toBe(5);
});
it('handles zero factor', () => {
const zero = createMultiplier(0);
expect(zero(100)).toBe(0);
});
});
Try It: Test This Function
Write comprehensive tests for this function:
function validatePassword(password) {
const errors = [];
if (!password) {
return { valid: false, errors: ['Password is required'] };
}
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain a number');
}
return {
valid: errors.length === 0,
errors
};
}
Loading Code Editor...
Testing with Parameterized Tests
Use test.each to run the same test with different inputs:
function isEven(n) {
return n % 2 === 0;
}
describe('isEven', () => {
test.each([
[2, true],
[4, true],
[0, true],
[-2, true],
[1, false],
[3, false],
[-1, false],
])('isEven(%i) should be %s', (input, expected) => {
expect(isEven(input)).toBe(expected);
});
});
// Output:
// ✓ isEven(2) should be true
// ✓ isEven(4) should be true
// etc.
You can also use a table format:
describe('calculate', () => {
test.each`
a | b | operation | expected
${1} | ${2} | ${'add'} | ${3}
${5} | ${3} | ${'sub'} | ${2}
${2} | ${4} | ${'mul'} | ${8}
${8} | ${2} | ${'div'} | ${4}
`('$operation($a, $b) = $expected', ({ a, b, operation, expected }) => {
expect(calculate(a, b, operation)).toBe(expected);
});
});
Key Takeaways
- Test the happy path first, then edge cases
- Always test boundary conditions (empty, null, min, max)
- Test error conditions with proper function wrapping
- Use
donecallback or async/await for callback testing - Use
test.eachfor parameterized tests to reduce duplication - Test higher-order functions by testing the returned functions
- Consider: what inputs could break this function?
Next, we'll tackle testing asynchronous code - promises, async/await, and timers!

