How To Write Unit Tests - Avoid Production Code in Tests
Using production code in test data setup is a bad idea because it makes tests depend on implementation details instead of the behavior they are meant to verify. When test data is built from the same code that the application uses, the tests become more likely to pass for the wrong reasons.
This article explores practical ways to keep your test data independent from production code so your tests stay focused on the behavior you want to verify.
Scenario
- A use case that fetches an Order and formats the returned details
- A unit test that verifies the use case
class GetOrderUseCase {
// ...
async execute(params: { orderId: string }): Promise<GetOrderResponse> {
const { orderId } = params;
const order = await this.orderRepository.get(orderId);
return {
orderId: order.id,
deliveryAddress: formatAddress(order.address),
};
}
}
export const formatAddress = (address: Address): string => {
const { street, houseNumber, city, zipCode } = address;
return `${street} ${houseNumber}, ${city}, ${zipCode}`;
};
it("should get an order - BAD", async () => {
// ...
const address = Address.create({
street: "Baker Street",
houseNumber: "221B",
city: "London",
zipCode: "12345",
});
const order = Order.create({
id: "1234567890",
address,
});
const expected = {
orderId: "1234567890",
deliveryAddress: formatAddress(address),
};
//..
const result = await useCase.execute({
orderId: "1234567890",
});
expect(result).toEqual(expected);
});
The test coverage is 100%.
Uncaught Production Code Issues
The formatter code was changed. The test still passes, and the coverage is still 100%. The change was deployed to production.
export const formatAddress = (address: Address): string => {
const { street, houseNumber, city, zipCode } = address;
return `${street} ${houseNumber}, ${city}, ${zipCode} DO NOT USE`;
};
When referencing production code in test data setup, the test coverage can be misleading. Since the code is used in tests, the coverage tools will mark those lines as being hit, even though the tests are not actually verifying the behavior of that code. This can lead to a false sense of security, as developers may believe that their code is well-tested, when in reality, it is not.
Test Data Independent from Production Code
The following unit test will fail if production code changes.
it("should get an order - GOOD", async () => {
// ...
const address = Address.create({
street: "Baker Street",
houseNumber: "221B",
city: "London",
zipCode: "12345",
});
const order = Order.create({
id: "1234567890",
address,
});
const expected = {
orderId: "1234567890",
deliveryAddress: "Baker Street 221B, London, 12345",
};
// ...
const result = await useCase.execute({
orderId: "1234567890",
});
expect(result).toEqual(expected);
});
- Do not use production code to create test data
- If setting up test data is difficult or verbose, consider adding methods to your code that will facilitate test data creation
- Use constant, hard-coded values for test data
Quite often functionalities like formatters or mappers are implemented separately from the main business logic.
This also means they are easier to test in isolation, especially if they handle edge cases. That way the main logic stays clean.
Full code samples can be found on GitHub.