FactoryJS - the TypeScript version of factory_bot
Tags: javascript, prisma, testing, typescript
Published: 2024-05-08 13:26:05


Recently, I've been developing a web app in TypeScript and struggled with writing clean tests. This article introduces factory-js and how to use this library.

---

## What is factory_bot?

[factory_bot](https://thoughtbot.com/upcase/videos/factory-bot) is a popular Ruby library for generating dummy models for tests.
Here is a simple example demonstrating how to use factory_bot:

```ruby
def bio(user)
  "I'm #{user.name}"
end

# 1. Define the user factory
factory :user do
  name "john"
  email { "#{name}@example.com" }
end

describe "#bio" do
  it "returns bio" do
    # 2. build the user model with defined factory.
    user = build(:user, name: "james")

    # 3. Write tests using the generated model.
    expect(bio(user)).to eq("I'm james")
  end
end
```

factory_bot has many advantages:

- **Time-saving**: Writing multiple lines of code just to create models is tedious. factory_bot can create models with almost a single line of code.

- **Maintainability**: Once a factory is defined, it can be reused across multiple tests. For instance, if you have to add new required columns to the user table, you just need to add new attributes to the user factory.

- **Readability**: factory_bot offers useful features like [traits](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#traits) and [dependent attributes](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#dependent-attributes), enhancing the readability of tests.

In fact, I use factory_bot in many of my projects, and it has become an indispensable library for writing highly maintainable tests.

--- 

## What about the TypeScript world?

These days, you can use well-crafted ORM libraries such as [Prisma](https://www.prisma.io/), [Drizzle](https://orm.drizzle.team/), [MikroORM](https://mikro-orm.io/), etc.

However, what about a testing library similar to factory_bot for TypeScript?

I have recently been developing an API for a hobby project in TypeScript and was looking for a library like factory_bot.

## Requirements

Ideally, I was looking for a factory library that met the following requirements.

- Provide as much of the same features as factory_bot as possible (e.g. traits) while ensuring type safety.
- Support multiple ORM libraries (e.g. Prisma, Drizzle).
- Capable of creating models with complex relations: e.g. N:1, N:M, STI, self-relationships.
- Supports type inference from objects, eliminating the need to define types manually.

After an extensive search, I could not find a factory library that met these requirements, so I decided to create my own.

## Is it really possible?

First, I looked into what APIs the popular ORMs have, and found that the APIs differ from ORM to ORM.

For example, Prisma returns a plain object, while TypeORM returns an instance of the class. Additionally, Prisma supports `connects`, which allows related models to be created simultaneously—a feature that DrizzleORM does not have.

These differences were enough to make my brain explode 🤯. I was wondering which ORM to design factory-js based on.

Ultimately, I realized that an ORM essentially acts as a wrapper for an SQL builder and is based on the CREATE TABLE DDL. A relational database (RDB) table basically consists of two elements: **columns** and **foreign keys** (although there is also a primary key, it is not crucial for factory-js).

Consequently, I added two features to factory-js: _props_ and _vars_. _props_ corresponds to columns and _vars_ corresponds foreign keys.

--- 

## API design

As shown in the following picture, the factory-js API is almost identical to the factory_bot API and is intuitive.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/comv6vx5i97avio62xq4.png)

### _props_

_props_ is a property of the object, corresponding to the columns of the table.

```typescript
const userFactory = await factory
  .define({
    props: {
      firstName: () => faker.person.firstName(),
      lastName: () => faker.person.lastName()
    },
    vars: {},
  })

const user = userFactory.build();
console.log(user) // { firstName: "...", lastName: "..." }
```

Each property value is wrapped in a function so that random generator libraries such as [faker](https://fakerjs.dev/) can be used.

It is also possible to override default property values in the method chain. This is useful when tests need to use fixed values rather than randomly generated values.

```typescript
it("returns firstName", async () => {
  const user = userFactory
    .props({ firstName: () => "John" })
    .build()

  expect(user.firstName).toBe("John")
})
```

### _vars_

This is to define variable and similar to factory_bot's [transient](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#transient-attributes). In contrast to _props_, variables are not added as properties to the built object.

Unfortunately, unlike factory_bot, variables cannot be used in the `.define` method but are usable in the method chain after the factory has been defined. 

```typescript
const userFactory = await factory
  .define({
    props: {
      // Set a temporary value so that TypeScript can infer types
      name: () => "",
    },
    vars: {
      title: () => "Mr.",
    },
  })
  .props({
    // Then, use a variable to set the actual value.
    name: async ({ vars }) => `${await vars.title} John`,
  });

const user = await userFactory.build();
console.log(user); // { name: 'Mr. John' }
```

This is due to a limitation of TypeScript: factory_bot does not have a type, but factory-js does, and factory-js infers types based on a object passed to `.define`. TypeScript tends to fail to infer the type if variables can be used in `.define`, because _vars_ and _props_ can have circular dependencies.

### _create_

Factory-js also has `.create` method, which is similar to factory_bot. In contrast to `.build`, this method inserts records into the database, so you can write tests that require stored records.

To use _create_, you need to pass a function as the second argument that insert a record into the database with the ORM you are using.

```typescript
const userFactory = await factory.define(
  {
    props: {
      firstName: () => "John",
      lastName: () => "Doe",
    },
    vars: {},
  },
  // This implementation depends on the ORM.
  async (props) => await db.user.create({ data: props }),
);

// Returns a saved object.
await userFactory.create();
```
---

## Examples

As explained in the previous section, we can define factories that have associations using only _props_ and _vars_ features. For example, the following factories have 1:1 relationships. A profile model belongs to a single user model.

```typescript
const userFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      role: () => 'admin',
    },
    vars: {},
  },
  async (props) => await db.user.create({ data: props })
)

const profileFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      userId: () => faker.number.int(),
      firstName: () => faker.person.firstName(),
      lastName: () => faker.person.lastName()
    },
    vars: {
      user: async () => await userFactory.create(),
    },
  },
  async (props) => await db.profile.create({ data: props })
).props({
  userId: async ({ vars }) => (await vars.user).id
});
```

In the example above, _vars_ is used to create the associated user model. The process is as follows:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4n1by7y820ci6i1o4t6g.png)

Let's compare with CREATE TABLE DDL. Where _props_ corresponds to columns and _vars_ to foreign keys.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4x9maykzncfsztlmq9d.png)

### Is _vars_ really required?

In fact, the use of _vars_ is not mandatory in the previous example. It is also possible to define a profile factory without the use of _vars_, as shown below:
 
```typescript
const profileFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      // create user model and set its id.
      userId: async () => await userFactory.create().id,
      firstName: () => faker.person.firstName(),
      lastName: () => faker.person.lastName()
    },
    vars: {},
  },
  async (props) => await db.profile.create({ data: props })
)
```

However, RDB tables sometimes have **[multiple foreign keys](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/one-to-one-relations#multi-field-relations-in-relational-databases)**, for example, the following user table has composite primary key: firstName and lastName. In this case, we have to use _vars_ to refer to the same user.

```typescript
const userFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      firstName: () => faker.person.firstName(),
      lastName: () => faker.person.lastName()
    },
    vars: {},
  },
  async (props) => await db.user.create({ data: props })
)

// BAD
const profileFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      // ❌ The user relationship cannot be created correctly
      //    because firstName and lastName
      //    refer to different users.
      firstName: () => (await userFactory.create()).firstName,
      lastName: () => (await userFactory.create()).lastName,
    },
    vars: {},
  },
  async (props) => await db.profile.create({ data: props })
)

// GOOD
const profileFactory = factory.define(
  {
    props: {
      id: () => faker.number.int(),
      firstName: () => '',
      lastName: () => ''
    },
    vars: {
      user: async () => await userFactory.create(),
    },
  },
  async (props) => await db.profile.create({ data: props })
).props({
  // ⭕ This works correctly because
  //    these columns refer to the same user.
  firstName: async ({ vars }) => (await vars.user).firstName,
  lastName: async ({ vars }) => (await vars.user).lastName,
});
```

Since it is tedious to use _vars_ and _props_ depending on whether it is a single or multiple foreign key, it is generally recommended to use _vars_.

## How does it work inside?

Factory-js uses two simple tricks internally to implement _vars_ and _props_: the [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and a simple memo function.

Proxy enable you to intercept the property access and customize returns values. [Proxy is also used in Vue3](https://vuejs.org/guide/extras/reactivity-in-depth#how-reactivity-works-in-vue).

The memo function is a small closure for caching calculated values so that _vars_ returns the same value no matter how many times it is called.

```typescript
export const memo = <V>(func: (...args: any) => V) => {
  let evaluated = false;
  let value: V | undefined = undefined;

  return (...args: A) => {
    // If this is the first call,
    // Execute the `func` and stores the result to `value`.
    if (!evaluated) value = func(...args);
    evaluated = true;
    return value;
  };
};
```

As you can see from [bundlephobia](https://bundlephobia.com/package/@factory-js/factory@0.2.1), factory-js is currently a very small package because it contains very simple logic.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vn5f53gmo2wjs624jkea.png)

--- 

## Other features

Other than _vars_ and _props_, factory-js has more powerful features that inspired by factory_bot.

### trait

I think the most useful feature of factory_bot is _trait_, and factory-js also supports _trait_.

_trait_ enable you to manage multiple _props_, _vars_ with a key and apply them with `.use`.

```typescript
const userFactory = await factory
  .define({
    props: {
      role: () => "guest",
      isAdmin: () => false,
    },
    vars: {},
  })
  .traits({
    admin: {
      props: {
        role: () => "admin",
        isAdmin: () => true
      }
    }
  })

const user = await userFactory.use((t) => t.admin).build();
console.log(user) // { role: "admin", isAdmin: "true" } 
```

### dependent props / vars

This is the most powerful feature of factory-js. Thanks to Proxy, you can define properties that depend on other properties.

The following example compute the fullName value from firstName and lastName.

```typescript
const user = await factory
  .define({
    props: {
      firstName: () => "John",
      lastName: () => "Doe",
      fullName: () => "",
    },
    vars: {},
  })
  .props({
    fullName: async ({ props }) =>
      `${await props.firstName} ${await props.lastName}`,
  });

const user = await userFactory
  // dependent props works correctly
  // even if the prop value is changed later.
  .props({ firstName: () => 'Tom' })
  .build();

// { firstName: 'Tom', lastName: 'Doe', fullName: 'Tom Doe' }
console.log(user); 
```

Are there really cases where this functionality is needed? In fact, I have experienced cases where I want to use dependent props to avoid validation errors.

For example, suppose an event log model has startTime and endTime columns and this model has a validation that checks if startTime is before endTime. In this case, you can use this feature to set endTime to one second before startTime to avoid validation errors.

---

## Prisma Plugin

As new features are added to the product, the database tables grow daily. I didn't want to define factories myself each time a new table was added.

So I created a prisma plugin for factory-js. The plugin generates factories based on the prisma schema file.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ui59a31bt2x7vk3dg41.png)


Factory-js only has a prisma plugin at the moment, but I think it would be possible to implement plugins for other ORMs such as DrizzleORM.

---

## Conclusion

So far I'm very happy with factory-js and am able to write clean tests with it. If you are interested in factory-js, you can try factory-js as it is open source.

{% embed https://github.com/factory-js/factory-js %}

I hope this article will interest and help you! 💖