Jest is a popular JavaScript testing framework that allows you to write unit tests. It comes with a suite of utilities that make testing easier. Here is a typical structure for a Jest test case:
Example Structure of a Jest Test Case
// Example test case using the mocked dependencies
it('should perform order cancellation', () => {
// Arrange: Set up the test scenario
// Act: Perform the action that triggers the unit under test
// Assert: Validate the outcomes using Jest expectations
});
Setup and Teardown
In testing, there are often preliminary actions (setup
) and concluding actions (teardown
) that need to be performed around your tests. Jest provides functions like beforeEach
and afterEach
for this purpose.
Setup (beforeEach)
: This is about preparing your testing environment before each test runs. For example, if you’re testing functions that rely on a database, you might need to initialize this database first.
beforeEach(() => {
initializeCityDatabase();
});
Teardown (afterEach)
: This involves cleaning up after each test. This might include actions like clearing a database to ensure one test’s data doesn’t affect another.
afterEach(() => {
clearCityDatabase();
});
You can find more information in the official Jest documentation over here.
Mocking in Jest
Mock functions in testing are tools that help you check the interactions between different parts of your code without relying on their actual implementations. Mock functions replace the real functions in your code during testing. They can:
- Simulate real behavior by returning specific values.
- Track calls to the function, along with the arguments passed during those calls.
- Monitor instances when the function is used as a constructor.
- Control return values during tests to check different scenarios.
// Basic mock function
const mockFunction = jest.fn();
// Mock function with return values
const mockFunctionWithReturn = jest
.fn()
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
// Asynchronous mock function
const asyncMock = jest.fn().mockResolvedValue('completed');
// Tests
describe('Mock Function Tests', () => {
test('basic mock function is called with correct argument', () => {
mockFunction('hello');
expect(mockFunction).toHaveBeenCalledWith('hello');
});
test('mock function returns specified values correctly', () => {
expect(mockFunctionWithReturn()).toBe(10); // First call
expect(mockFunctionWithReturn()).toBe('x'); // Second call
expect(mockFunctionWithReturn()).toBe(true); // Subsequent calls
});
test('asynchronous mock function resolves to expected value', async () => {
await expect(asyncMock()).resolves.toBe('completed');
});
});
Explore more information in the official doc over here.
Using spyOn
The jest.spyOn
function is a powerful feature in Jest that lets you create a mock function that also keeps track of calls to a method on an object. This spy function not only monitors how a method is used, but it can also run the original method, unlike other mocking approaches that simply replace the method.
Read more about it from these links:
Checking how many times a function was called
Use the toHaveBeenCalledTimes
method to specify the expected number of times a function should have been invoked during the test. This helps ensure that your function is called the correct number of times.
expect(orderRepository.insertActivityLogs).toHaveBeenCalledTimes(1);
Checking the result of a function call
To verify the outcome returned by a function, use the expect function followed by a matcher like toBe
. This confirms that the function returns the expected result when invoked with specific arguments.
const result = await retailerOrderStrategy.insertCancellationLogs(
ctx,
orderIds,
ordersInfo,
orderCancellationDto
);
expect(result).toBe(true);
Checking if a function is not called
To ensure a function was not called with certain arguments, use not.toHaveBeenCalledWith
. This is useful for verifying that a function does not execute with specific parameters in certain scenarios.
expect(orderRepository.insertActivityLogs).not.toHaveBeenCalledWith(ctx, [
{
payload: 'Out of stock unmarked due cancel order',
changes: JSON.stringify({ isStock: false }),
sourceId: orderItems[0].skuId,
},
{
payload: 'Out of stock unmarked due cancel order',
changes: JSON.stringify({ isStock: false }),
sourceId: orderItems[1].skuId,
},
]);
Illustrating the type of values passed/returned by a function
In the snippet below, we expect a function to be called with a set of parameters and one of the parameters have been specified to be a date:
expect(orderRepository.decrementAndIncrementUpdate).toHaveBeenCalledWith(
ctx,
{ id: '2' },
'current_aoos_inv',
13,
'aoos_limit',
{ updated_at: expect.any(Date) }
);
A Gotcha with Mock Functions
When using Jest to track how many times a function is called(using toHaveBeenCalledTimes
and toHaveBeenCalledWith
) or to check the arguments it was called with, the function in question must be a mock or a spy. If you attempt these checks on a regular function, Jest will throw an error. Here’s the typical error message you’ll see if you mistakenly apply these matchers to a non-mocked function:
expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function validateOrderCancelPayload]
Testing for Specific Errors with Mocked Functions
When you anticipate that a mocked function should throw an error under certain conditions, it’s beneficial to specify the type or message of the expected error. This not only confirms the presence of an error but also validates that the correct error is thrown, which is crucial for maintaining robust error handling in your application.
// Suppose `cancelOrder` is mocked to throw an error when a cancellation is invalid
jest.spyOn(retailerOrderStrategy, 'cancelOrder').mockImplementation(() => {
throw new Error('Invalid cancellation attempt');
});
// Test that the specific error is thrown
await expect(
retailerOrderStrategy.cancelOrder(ctx, orderCancellationDto, type)
).rejects.toThrowError('Invalid cancellation attempt');