TypeScript Testing Tips - Creating Dummies

This 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 emphasise what's important and avoid incidental details.

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 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.

Article By
blog author

Eoin Mullan

Principal Engineer