Avoiding the any keyword in Typescript tests
Using the any type in TypeScript tests can lead to less maintainable and error-prone code. Let's explore how to avoid it and improve our test quality.
When we use any
in TypeScript, we are telling the compiler to opt out of type checking. Going back to using plain JavaScript.
This is defeating the purpose of using TypeScript in the first place.
I totally sided with this approach, but I was open to the idea of using any
in tests. It’s not production code after all
and the tests are focused only on the data required to run the tests, not the actual types.
There is a better way to handle this, and it is to use data fixtures.
Creating data fixtures
The simplest way to create data fixtures is to hand craft them using factory functions and partial building.
This is a great way to ensure that the data is valid and meets the requirements of the tests without having to use any
.
Let’s take a look at an example:
type User = {
id: string;
name: string;
email: string;
};
function createUser(overrides: Partial<User> = {}): User {
return {
id: 'default-id',
name: 'Default Name',
email: 'default@example.com',
...overrides
};
}
This function allows us to create a User
object with default values, while also allowing us to override any of the properties we want.
When it comes to testing, we may be interested in only a few of the properties because the production code is operating on a subset of the data. This is where we can use pass the overrides:
const user = createUser({ name: 'John Doe', email: 'john.doe@example.com' });
This would still yield a valid user object with the default id
but with the overridden name
and email
properties.
Given all the AI tooling available today, you can generate these factory functions quickly and easily.
Using faker
This is fairly static data but we can make it a bit more interesting by using the faker
library to generate random data.
import { faker } from '@faker-js/faker';
type User = {
id: string;
name: string;
email: string;
};
function createUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
...overrides
};
}
This could test the robustness of the code against a wider range of data, while still ensuring that the data is valid and meets the requirements of the tests. It could result in non-deterministic tests, but that is a trade-off worth considering.
Using zod-mock
Zod has become a standard library for schema validation in Typescript. Many developers use it to define their data structures and infer types from them. A library that could be paired with this is zod-mock, which can generate mock data based on Zod schemas.
import { z } from 'zod';
import { mock } from '@anatine/zod-mock';
type User = z.infer<typeof userSchema>;
const userSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
});
function createUser(overrides: Partial<User> = {}): User {
const user = mock(userSchema);
return { ...user, ...overrides };
}
This approach allows you to define your data structure using Zod and then generate mock data based on that schema. It combines the benefits of type safety and ease of use, making it a powerful tool for testing.
Conclusion
After reading this, there are not many excuses to continue using any
in your TypeScript tests.
Looking at that last example above, there is not a lot of boilerplate code to create a data fixture. So stop using any
and start using data fixtures to create valid test data. This will help you write better tests, catch more bugs, and
improve the overall quality of your codebase.
Happy testing! 🧑💻