Make a Factory: Creating Tests and Stories with Factory Functions in TypeScript and React
Tags: typescript, react, javascript, jest
Published: 2023-06-13 01:53:23

Writing reliable tests in TypeScript can be a challenging task. It often involves complex tasks like [mocking modules](https://jestjs.io/docs/manual-mocks), [stubbing API responses](https://dev.to/zaklaughton/the-only-3-steps-you-need-to-mock-an-api-call-in-jest-39mb), and [spying on functions](https://jestjs.io/docs/jest-object#jestspyonobject-methodname).

However, by using factory functions, we can achieve consistency, readability, and type safety in our TypeScript and React tests.

In this article, I'll demonstrate how I use a simple creator function to generate factories and speed up the test writing and storybook creation processes. 

## Previous Approach: Lots of ad-hoc stuff

In the past, I relied on building numerous small factory functions for usage in Storybook or component tests. While this approach allowed me to build typed objects for tests and Storybook stories, it also introduced a considerable amount of code to review and maintain. 

With each new test scenario, I found myself defining new functions and types, which increased the complexity and made it harder to encourage consistency across our codebase.

## Looking for a Solution: npm packages

In search of a solution, I explored various TypeScript libraries that could handle factory logic. One such library was [fishery](https://www.npmjs.com/package/fishery), which is really well designed. 

However, I realized that it offered more functionality than I needed for my use case. So I decided to create my own custom factory function that suited our specific requirements.

## Introducing `makeFactory`: the factory creator

This factory creator function, called `makeFactory`, returns a factory function, such as `createUser`, which can be invoked to generate new objects.

It accepts a default object argument that can be overridden, providing flexibility for different scenarios. For instance, we can explicitly define the object's type using a generic with `makeFactory<IUser>({ name: 'Jane', age: 40 })`.

Here's the implementation of the makeFactory function:

```tsx
import merge from "lodash/merge";
import cloneDeep from "lodash/cloneDeep";

export function makeFactory<T>(
  defaultArgs: T
): (args?: RecursivePartial<T>) => T {
  return (args = {}) => merge({}, cloneDeep(defaultArgs), args);
}
```

By utilizing this pattern, we can create a function like `createUser` that generates an object with the `IUser` type:

```tsx
const createUser = makeFactory<IUser>({
  name: "George Rosin",
  type: "customer",
  age: 25,
});
```
And when called, it looks something like this:
```tsx
const USER_ADMIN = createUser({ type: 'admin' })
// { name: 'George Rosin', type: 'admin', age: 25 }
```

The `makeFactory` function's argument defines the default properties assigned to the object returned from `createUser`. Any properties passed to the `createUser` function will override the default properties, allowing for easy definition of different data scenarios for tests.

## Usage in Storybook: speed things up!

This factory function pattern not only simplifies object creation but also enhances readability. In the context of Storybook stories, we can define different scenarios with ease. Here's a quick example:

```tsx
// UserLabel.stories.tsx

import type { Meta, StoryObj } from "@storybook/react";
import UserLabel, { IUserLabelProps } from "./UserLabel";
import createUser from "./createUser";

const meta: Meta<typeof UserLabel> = {
  title: "UserLabel",
  component: UserLabel,
};

export default meta;

type Story = StoryObj<typeof UserLabel>;

export const Admin: Story = {
  args: {
    user: createUser({ type: "admin" }),
  },
};

export const User: Story = {
  args: {
    user: createUser({ type: "customer" }),
  },
};
```

By utilizing the factory function `createUser`, we can easily define different scenarios while maintaining readability in our Storybook stories.

## Usage in Component Tests: faster tests!

The power of factory functions extends to React component tests as well. Just like in the Storybook stories, we can explicitly define the differences between scenarios. Here's an example:

```tsx
// UserLabel.spec.tsx

import { render, screen } from "@testing-library/react";
import UserLabel, { IUserLabelProps } from "./UserLabel";

describe("<UserLabel />", () => {
  it("renders an 'admin' label", () => {
    const USER_ADMIN = createUser({ type: "admin" });
    render(<UserLabel user={USER_ADMIN} />);
    expect(screen.getByTestId("UserLabel")).toContain("George John - Admin");
  });

  it("renders a 'customer' label", () => {
    const USER_CUSTOMER = createUser({ type: "customer" });
    render(<UserLabel user={USER_CUSTOMER} />);
    expect(screen.getByTestId("UserLabel")).toContain("George John - Customer");
  });
});
```

With this approach, even when dealing with complex API models or types, testing becomes much simpler and more efficient.

## Going further: nested functions

When dealing with complex interface types with nested objects, it can also be useful to use factories inside of other factories. Here's an example of that pattern:

```tsx
interface IAddress {
  addressLine1: string;
  state: string;
  city: string;
  postalCode: string;
}

interface IBusiness {
  id: string;
  name: string;
  address: IAddress;
}

const createAddress = makeFactory<IAddress>({
  addressLine1: '123 Broadway',
  state: 'NY',
  city: 'Chester',
  postalCode: '10920'
});

const createBusiness = makeFactory<IBusiness>({
  id: '123',
  name: 'Pizza Inc.',
  address: createAddress()
});
```

While the above example is a bit simple, the modularity of the functions creates a reusability that can be really helpful. 

Additionally, because of `lodash`'s `merge` function, we can pass in partial values and override nested object properties, like this:

```tsx
const BUSINESS_PA = createBusiness({ address: { state: 'PA' }})
/**
 * { 
 *   id: '123', 
 *   name: 'Pizza Inc.', 
 *   address: { 
 *     addressLine1: '123 Broadway', 
 *     state: 'PA', 
 *     city: 'Chester', 
 *     postalCode: '10920' 
 *   }
 * }
 */
```

## In conclusion: make a factory 🏭

By leveraging factory functions, we can create reliable tests with consistency, readability, and type safety in TypeScript and React. The `makeFactory` pattern provides a flexible and maintainable solution for generating typed objects that will speed up your developer workflow for writing tests and Storybook stories.

## Bonus: Use ChatGPT to generate default values

Here is a prompt I have found useful for generating realistic default values depending on the property name: 

> 🤖 For the following code, create a typed object for each interface with typed values that are realistic based on the property names.

```tsx
interface IAddress {
  addressLine1: string;
  state: string;
  city: string;
  postalCode: string;
}

interface IBusiness {
  id: string;
  name: string;
  address: IAddress;
}
```
This returned the following answer from ChatGPT: 

> Sure! Here are typed objects for each interface with realistic property values:

```tsx
const address: IAddress = {
  addressLine1: "123 Main Street",
  state: "California",
  city: "Los Angeles",
  postalCode: "90001",
};

const business: IBusiness = {
  id: "123456789",
  name: "ABC Company",
  address: address,
};
```
🤯 Not bad!