Here at Instil, we’re big fans of TypeScript. Although we appreciate the strengths of dynamic languages, all things considered, strong static typing wins more often than not.
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:
export interface Employee {
id: string;
name: string;
department: Department;
position: Position;
}
Now, create a builder like this:
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
can simply call
const dummyEmployee = buildEmployee();
without supplying any parameters and they’ll get a valid Employee
object with the default parameters.
The Partial
class in the builder function means that the input parameter is a version of Employee
where all properties are optional. This means consumers can override specific properties relevant to what's being tested, e.g.
const employeeJane = buildEmployee({ name: "Jane" });
This allows your test to emphasise only what’s important to it, without unnecessary clutter.
Note that Department
and Position
use the same pattern, and it’s builders all the way down, allowing consumers to be explicit about sub-components if they need to.
Using These Dummies in Tests
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, but instead let them be
undefined
and cast your object to the return type before it’s returned. This will force tests 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 anEmployee
'sid
andname
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 interface
s
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.