TypeScript Testing Tips - Creating Dummies
Join the DZone community and get the full member experience.
Join For FreeThis is the first post in a series on using TypeScript in practical applications. It shows a simple TypeScript pattern for building type-safe, unit test dummies. The focus is on simplicity for the consumer so tests emphasize what's important and avoid incidental details.
When it comes to a powerful type system few mainstream languages come close to TypeScript. So, in this series of posts, we’re going to document how we use TypeScript practically in real projects.
First up…
Testing With TypeScript
Seasoned TypeScripters may be surprised to learn that the T in TDD can also stand for Test, and we all write those first, right? So that’s where we’ll start.
The first few posts in the series will be recipes we rely on to remove boilerplate from our tests so they stay on point, all while maintaining the benefits of static typing. This post will start simple, looking at how we create dummy objects. Subsequent posts will look at bringing static types to more advanced mocking techniques with Jest.
Creating Dummy Types
You’ll often need to create dummy objects in your test code. Something to pass into the method you’re testing, or to have returned by a mock function. This pattern, which uses the Partial utility type, allows tests to create the object they need while specifying only the properties they care about. Consider an employee interface:
xxxxxxxxxx
export interface Employee {
id: string;
name: string;
department: Department;
position: Position;
}
Now, create a builder like this:
xxxxxxxxxx
export function buildEmployee(
{
id = "abc123",
name = "Jim",
department = buildDepartment(),
position = buildPosition(),
}: Partial<Employee> = {}
): Employee {
return {
id,
name,
department,
position,
}
}
Now, tests that need an Employee have the facility to create what they need in a very concise way, emphasizing only what’s important, without unnecessary clutter.
Using These Dummies in Tests
In the simplest case, you may just want an Employee object without caring about any of its property values. This can be useful e.g. if you want to verify an object is being passed through to a dependency correctly. In this case, you can call buildEmployee() without any parameters, and all the defaults will be used.
xxxxxxxxxx
it("should fetch employee pay from payroll service", () => {
const dummyEmployee = buildEmployee();
target.buildPayslip(dummyEmployee);
expect(payrollService.calculatePay).toHaveBeenCalledWith(dummyEmployee);
});
This verifies that the same employee object your test passes to buildPayslip
is then further passed onto the payrollService dependency. Similarly, a simple employee object with default properties can be useful when you need to verify that the object returned by a dependency to your code under test is then further passed back to the calling code.
Another common use case for dummy objects is when you need to ensure the code your testing accesses an object’s properties correctly. The Partial<Employee> type on the input parameter to the builder function means the input parameter is a variation of Employee where all properties are optional. It allows consumers to override specific properties relevant to what’s being tested while taking defaults for all other properties. For example, consider a function that searches for all records containing an employee’s name.
xxxxxxxxxx
it("should find all records containing employee's name", () => {
const dummyEmployee = buildEmployee({
name: "Jane"
});
target.findRecords(dummyEmployee);
expect(recordService.findAllMatchingRecords("Jane"));
});
In this test, we wish to be explicit about the employee’s name only as all other properties are irrelevant, and the builder lets us do this. The test is self-contained in that we can easily make the visual link between where the employee’s name is set and where it’s verified. We don’t need to navigate to where the dummy was created to see what values it was initialized with.
Finally, note that Department and Position use the same pattern, and it’s builders all the way down. This allows consumers to be explicit about sub-components if they need to, e.g.
xxxxxxxxxx
it("should build employee summary", () => {
const dummyEmployee = buildEmployee({
name: "Jane",
department: buildDepartment({
name: "R&D",
division: "Skunkworks",
}),
position: buildPosition({
name: "Software Engineer",
}),
});
expect(target.buildSummary(dummyEmployee))
.toEqual("Jane (Software Engineer): R&D - Skunkworks");
});
Best Practice for This Pattern
Before you copy, paste, and get on your way, it’s worth a quick think on how we use these dummy values. Tests should not rely on default dummy values but, instead, should explicitly define any properties relevant to them. So a good rule to keep in mind is:
Changing a default scalar in any of your dummy builders shouldn’t cause any tests to fail.
This requires some discipline on the part of developers and, if you wish, you can remove that burden in one of the following ways:
- Don’t provide defaults for scalars in the dummy object builder function, but instead let them be undefined and cast your object to the return type before it’s returned. This will force consumers to be explicit about properties they need but it can also make tests a bit more noisy as they may need to define properties that are required by a method but not relevant to a test, e.g. logging an Employee’s id and name while building their payslip. It also means your dummies are invalid objects.
- Include builders for your scalers, e.g. buildString, buildNumber, etc. This means all your default strings will be the same, so you can’t think of the employee’s name as “Jim” anymore.
There’s room for debate here, and it could be worth having the development team explore what will work best for them.
Beyond Interfaces
While interfaces are the simplest case it’s possible to use this pattern with classes too. But this typically requires a few more moving parts involving a mocking framework, and that’s what we’ll be getting to next time.
Published at DZone with permission of Eoin Mullan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments