Table of Contents
Testing is a crucial part of software development, ensuring that code functions correctly under various conditions. When using Bun for testing JavaScript applications, implementing mocking and stubbing becomes essential, especially for complex scenarios involving external dependencies or intricate logic.
Understanding Mocking and Stubbing
Mocking and stubbing are techniques used to isolate the code under test by replacing real dependencies with controlled stand-ins. This approach allows developers to simulate different responses and behaviors, making tests more reliable and focused.
Implementing Mocks in Bun
Bun provides flexible APIs for mocking modules and functions. To mock a module, you can use the bun.mock function, which intercepts imports and replaces them with mock implementations.
Example:
import { describe, it, expect, vi } from 'bun:test';
bun.mock('external-api', () => ({
fetchData: () => Promise.resolve({ data: 'mocked data' }),
}));
import { fetchData } from 'external-api';
describe('fetchData', () => {
it('returns mocked data', async () => {
const result = await fetchData();
expect(result.data).toBe('mocked data');
});
});
Stubbing Functions for Complex Behaviors
Stubbing involves replacing specific functions with custom implementations that simulate complex behaviors, such as delayed responses, errors, or conditional logic. This technique is useful for testing edge cases and error handling.
Using vi.fn() from Bun’s testing library, you can create stubs with tailored behavior.
Example:
import { describe, it, expect, vi } from 'bun:test';
const fetchDataStub = vi.fn()
.mockImplementationOnce(() => Promise.resolve({ data: 'first call' }))
.mockImplementationOnce(() => Promise.reject(new Error('Network error')))
.mockImplementation(() => Promise.resolve({ data: 'subsequent call' }));
describe('fetchDataStub', () => {
it('returns first mocked response', async () => {
const result = await fetchDataStub();
expect(result.data).toBe('first call');
});
it('handles error response', async () => {
try {
await fetchDataStub();
} catch (error) {
expect(error.message).toBe('Network error');
}
});
it('returns subsequent responses', async () => {
const result = await fetchDataStub();
expect(result.data).toBe('subsequent call');
});
});
Testing Complex Scenarios
Combining mocks and stubs allows testing of complex scenarios such as multi-step workflows, error handling, and external API failures. Carefully designing your mocks and stubs ensures comprehensive test coverage.
For example, simulating a sequence of API responses can be achieved by chaining mockImplementationOnce calls, enabling tests to cover various states of your application.
Example: Simulating Sequential API Calls
Here’s how to simulate multiple responses in a sequence:
const apiMock = vi.fn()
.mockImplementationOnce(() => Promise.resolve({ status: 200, body: 'First response' }))
.mockImplementationOnce(() => Promise.reject(new Error('Timeout')))
.mockImplementation(() => Promise.resolve({ status: 200, body: 'Final response' }));
// Usage in test
describe('Sequential API responses', () => {
it('handles first response', async () => {
const response = await apiMock();
expect(response.body).toBe('First response');
});
it('handles error response', async () => {
try {
await apiMock();
} catch (error) {
expect(error.message).toBe('Timeout');
}
});
it('handles final response', async () => {
const response = await apiMock();
expect(response.body).toBe('Final response');
});
});
Implementing effective mocking and stubbing strategies in Bun enables thorough testing of complex application behaviors, making your code more robust and reliable.