Defining Variables
To define variables, include them in the vars
property within .define
. Defined variables can be referenced when generating property values in .props
. In the example below, a variable called greeting
is defined and used to generate the value of the bio
property.
import { factory, later } from "@factory-js/factory";
const userFactory = factory
.define({
props: {
// At this point, the `greeting` variable cannot be referenced yet, so only the type is defined
bio: later<string>(),
},
vars: {
// Define a variable called `greeting`
greeting: () => "I'm",
},
})
.props({
// Reference the `greeting` variable to generate the value of `bio`
bio: async ({ vars }) => `${await vars.greeting} John`,
});
console.log(await userFactory.build()); // 👉 { bio: "I'm John." }
Note that you cannot reference variables within .define
. Therefore, the bio
property in the above .define
uses later
to define only the type.
For more details on how to use later
, refer to Placeholder.
Overwriting Variables
To overwrite the defined variables in a factory, use .vars
. In the example below, the value of greeting
is overwritten to Hello I'm
.
import { factory } from "@factory-js/factory";
const userFactory = factory
.define({
props: {
bio: later<string>(),
},
vars: {
greeting: () => "I'm",
},
})
.props({
bio: async ({ vars }) => `${await vars.greeting} John`,
});
const user = await userFactory.vars({ greeting: () => "Hello I'm" }).build();
console.log(await userFactory.build()); // 👉 { bio: "Hello, I'm John." }
.vars
can be called multiple times in a method chain. If the same variable is overwritten multiple times, the last value takes precedence.
const user = await userFactory
.vars({ greeting: () => "I'm" })
.vars({ greeting: () => "Hi, I'm" })
.vars({ greeting: () => "Hello I'm" })
.build();
console.log(await userFactory.build()); // 👉 { bio: "Hello, I'm John." }
You cannot add new variables with .vars
. You can only overwrite the values
of variables defined in .define
. If you want to add variables, refer to
Extending Factory.
Using Variables with ORM
Variables are most useful when combined with ORM, especially when defining factories that have relationships with other models. In the example below, factories for a user and the user’s profile are defined. To create a user’s profile, the owning user is needed, so the user object is stored in a variable and referenced in the userId
property.
import { factory, later } from "@factory-js/factory";
// Define the user factory
const userFactory = factory.define(
{
props: {
id: () => 1,
name: () => "John",
role: () => "employee",
},
vars: {},
},
async (user) => {
return db.user.create({ data: user });
},
);
// Define the profile factory
const profileFactory = factory
.define(
{
props: {
userId: later<string>(),
bio: () => "Hello",
},
vars: {
// Store a default user in a variable for this profile
user: () => await userFactory.create(),
},
},
async (profile) => {
return db.profile.create({ data: profile });
},
)
.props({
// Reference the stored user's ID
userId: async ({ vars }) => (await vars.user).id,
});
console.log(await profileFactory.create()); // 👉 { id: 1, bio: "Hello" }
You can also change the user to any desired user with .vars
. Below is a test that verifies if true
is returned when the profile belongs to an admin user.
import { expect, it, describe } from "vitest";
import { userFactory, profileFactory } from "../factories";
import { isAdminProfile } from "./is-admin-profile";
describe("when a profile is for admin", () => {
it("returns true", async () => {
const admin = await userFactory
.props({ id: () => 2, role: () => "admin" })
.create();
const profile = await profileFactory.vars({ user: () => admin }).create();
expect(isAdminProfile(profile)).toBe(true);
});
});
Caching and Efficiency
FactoryJS caches and reuses variables efficiently. This ensures that a variable is calculated only once, maintaining idempotency even if referenced multiple times. In the example below, the cardId and roomId properties have the same number, as they both reference the same id variable.
let count = 1;
const userFactory = factory
.define({
props: {
id: later<number>(),
cardId: later<string>(),
roomId: later<string>(),
},
vars: {
// It seems `count` would increment each time,
// but it is cached and calculated only once
id: () => count++,
},
})
.props({
id: async ({ vars }) => await vars.id,
cardId: async ({ vars }) => `card:${await vars.id}`,
roomId: async ({ vars }) => `room:${await vars.id}`,
});
console.log(await userFactory.build()); // 👉 { id: 1, cardId: 'card:1', roomId: 'room:1' }
console.log(await userFactory.build()); // 👉 { id: 2, cardId: 'card:2', roomId: 'room:2' }
Therefore, you don’t need to worry about the number or order of variable calls. FactoryJS handles caching appropriately, allowing you to define factories intuitively.
Additionally, unused variables are not calculated. In the example below, executing the id
variable would log CALL
to the console, but since id
is never used, nothing is logged. You don’t need to worry about unnecessary calculations for variables.
const userFactory = factory.define({
props: {
name: () => "John",
},
vars: {
id: () => {
console.log("CALL");
return 1;
},
},
});
console.log(await userFactory.build()); // Nothing is logged to the console
Defining Variables Dependent on Other Variables
You can also define variables that reference other variables. In the example below, the value of actualPrice
is generated based on the values of price
and discount
.
const itemFactory = factory
.define({
props: {
label: later<string>(),
},
vars: {
price: () => 100,
discount: () => 0.1,
actualPrice: later<number>(),
},
})
.vars({
actualPrice: async (vars) =>
(await vars.price) * (1 - (await vars.discount)),
})
.props({
label: async ({ vars }) => `$${await vars.actualPrice}`,
});
const item = await itemFactory.build();
console.log(item); // 👉 { label: '$90' }
Avoid creating variables that reference each other circularly, as this could result in an infinite loop.