Test factories
Orchid ORM
ecosystem offers a library for setting up JavaScript objects, to use these objects in tests.
It is producing objects of the shape defined by your table columns.
Table schemas for input (inserting) and output (selecting) may vary, the test factory is using the input schema.
Random values are produced by faker.js, you can use faker.seed(1)
to fix the randomness in a test for producing same values.
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db);
const user = factory.user.build();
// user is an object with random values, like:
// {
// id: 89613,
// name: 'Jackie Homenick',
// password: 'MHDDzAPYHzuklCN',
// }
// save the user with random values to the database
const createdUser = await factory.user.create();
// create many users with specific emails
const manyUsers = await factory.user.createMany(
{ email: 'one@email.com' },
{ email: 'two@email.com' },
);
Both build
and create
methods will handle the timestamp field in a certain way:
- if the record contains multiple timestamps (such as
createdAt
andupdatedAt
) the value will be equal for each field; - if you have columns configured for timestamps as numbers (
t.timestamp().asNumber()
) the fields will have an equal numeric timestamp; t.timestamp()
returning strings andt.timestamp().asDate()
returningDate
objects both produce equal dates.
Each newly generated object will have a timestamp increased by 1 millisecond, so creating a list of records and then testing a query that is ordered by timestamp should work just fine.
By default, all text columns will be limited to generate 1000-character long strings at most. You can override the maximum limit by specifying maxTextLength
:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db, {
maxTextLength: 123,
});
example
This example is extracted from building a sample app, you can find more test examples in that doc.
Here we are using build
to build parameters for a test request, and create
to create records for testing how unique violations are handled.
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db);
describe('registration', () => {
const params = factory.user.pick({
username: true,
email: true,
password: true,
});
it('should register a new user', async () => {
// buid a new random user data:
const data = params.build();
// testRequest may be a wrapper around light-my-request, axios, supertest
// perform a POST request to /users with the data:
const res = await testRequest.post('/users', data);
const json = res.json();
// expect response to have the same data as we sent:
expect(json).toMatchObject({
username: data.username,
email: data.email,
});
// expect database to have a newly registered user with proper fields:
const savedUser = await db.user.findBy({ username: data.username });
expect(savedUser).toMatchObject({
username: data.username,
email: data.email,
});
});
it('should return error when username is taken', async () => {
// build a new random user data:
const data = params.build();
// create a new user with a random data, but this specific username:
await factory.user.create({ username: data.username });
const res = await testRequest.post('/users', data);
// expect response to be failed with a message:
expect(res.statusCode).toBe(400);
expect(res.json()).toMatchObject({
message: 'Username is already taken',
});
});
});
setup
Install this library and the faker.js:
npm i -D orchid-orm-test-factory @faker-js/faker
Export factory
from some file where you have utilities for tests:
// src/utils/test-utils.ts
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
export const factory = ormFactory(db);
overriding generators per table
To define a custom data generator for a particular table:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db, {
extend: {
myTableName: {
// called every time when generating data for this column
columnName(sequence) {
// utilizing the sequence incrementing value to keep data unique
return `custom string ${sequence}`;
},
},
},
});
json
If you're not using zod schemas integration for the ORM, you can define a custom function to return the needed JSON shape for a table:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db, {
extend: {
myTableName: {
// called every time when generating data for this column
jsonColumnName(sequence) {
// utilizing the sequence incrementing value to keep data unique
return {
number: sequence,
text: `some text ${sequence}`,
};
},
},
},
});
In case you're using zod schemas with the ORM, you can install and integrate @anatine/zod-mock to automatically generate JSON data based on defined schemas.
npm install @anatine/zod-mock
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
import { ZodAnyType } from 'zod';
import { generateMock } from '@anatine/zod-mock';
const factory = ormFactory(db, {
fakeDataForTypes: {
jsonb(column) {
const zodSchema = column.inputSchema as ZodAnyType;
return () => generateMock(zodSchema);
},
},
});
If you're using valibot schemas, there is a library for it as well: valimock.
npm i -D valimock
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
import { BaseSchema } from 'valibot';
import { Valimock } from 'valimock';
const factory = ormFactory(db, {
fakeDataForTypes: {
jsonb(column) {
const valibotSchema = column.inputSchema as BaseSchema;
const valimock = new Valimock();
return () => valimock.mock(valibotSchema);
},
},
});
sequence
Internally the factory keeps a sequence
number which is increased by 1 for each new record.
The sequence can be used when overriding field values with custom functions:
const records = factory.user.buildList(3, {
id: (sequence) => sequence,
email: (sequence) => `email-${sequence}@mail.com`,
});
In a such way, each record can have a unique id
and email
.
Modern test frameworks and Jest
are running test suites in parallel, and this can lead to a situation when 2 test suites are trying to save a record with the same email-1@mail.com
email to the database.
This problem is handled specifically for Jest
by using the process.env.JEST_WORKER_ID
env variable: if this var is defined, orchid-orm-test-factory
will start the sequence from (workerId - 1) * sequenceDistance + 1
, where sequenceDistance
is 1000 by default. In such a way, the first suite sequence will start from 1, the second suite sequence will start from 1001, and so on.
sequenceDistance
for the described equation can be overridden:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db, {
sequenceDistance: 123456,
});
For other test frameworks which are running suites in parallel provide sequence
manually when creating a factory:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
// use VITEST_POOL_ID for vitest framework, this env var behaves like JEST_WORKER_ID in jest
const workerId = parseInt(process.env.VITEST_POOL_ID as string);
const factory = ormFactory(db, {
sequence: (workerId - 1) * 1000 + 1,
});
build
Build a new object with the same structure as your table filled with random data:
import { ormFactory } from 'orchid-orm-test-factory';
import { db } from '../path-to-db';
const factory = ormFactory(db);
const user = factory.user.build();
Optionally you can pass specific data to build
:
const specificUser = factory.user.build({
name: 'James',
age: 30,
});
You can provide a function to generate new value:
const user = factory.user.build({
randomNumber: () => Math.random(),
});
It's possible to provide extra data, which is not defined by table columns:
const user = factory.user.build({
customField: 'someValue',
});
buildMany
Build multiple records with buildMany
. It is accepting the same parameters as build
, but can accept multiple arguments.
const [user1, user2, user3] = factory.user.buildMany(
// empty: all data is generated
{},
// override data
{
name: 'James',
},
// return dynamic value from a function
{
age: () => Math.ceil(Math.random() * 100),
},
);
buildList
Build an array of objects, and provide a number for how many objects are needed:
const arrayOfUsers = factory.user.buildList(5);
The optional second argument is the same as in build
:
const arrayOfCustomizedUsers = factory.user.build(5, {
// each user in the array will have their random number
randomNumber: () => Math.random(),
});
create
create
is saving record to the database and returns the result:
const user = await factory.user.create();
In the argument, you can provide values for columns, functions to generate values, and you can use all the nested create methods available for this table.
In contrast to build
, additional properties are not allowed here, only the columns of the table.
The create
method will automatically look for identity and serial primary keys in the table to omit it from being generated, so the natural sequence of t.identity().primaryKey()
columns will be preserved.
// create a user with a profile (user hasOne profile) and genres (user hasMany genres)
const customizedUser = await factory.user.create({
name: 'Mikael',
age: () => 49,
profile: {
create: {
bio: 'Eros Ramazzotti of Sweden',
},
},
genres: {
create: [
{
name: 'progressive metal',
},
{
name: 'progressive rock',
},
],
},
});
createMany
Create multiple records at once with createMany
, it executes a single INSERT
statement.
const [user1, user2, user3] = await factory.user.createMany(
// empty: all data is generated
{},
// override data
{
name: 'James',
},
// return dynamic value from a function
{
age: () => Math.ceil(Math.random() * 100),
},
);
createList
Create an array of records, and provide a number for how many objects are needed:
const users = await factory.user.createList(5);
The optional second argument is the same as in create
:
const arrayOfCustomizedUsers = await factory.user.create(5, {
// each user in the array will have their random number
randomNumber: () => Math.random(),
});
You can dynamically create a belongsTo
record and use its id:
// create 5 books with 5 different authors
const books = await factory.book.create(5, {
author: async () => (await factory.author.create()).id,
});
unique columns
Test factory will prefix unique text columns with sequence, and it will use a sequence for unique numeric columns.
Example:
class SomeTable extends BaseTable {
readonly table = 'table';
columns = this.setColumns((t) => ({
id: t.identity().primaryKey(),
text: t.text(),
email: t.string().email().unique(),
url: t.varchar(1000).url().unique(),
number: t.integer().unique(),
greaterThan10: t.integer().gt(10).unique(),
greaterThanOrEqualTo10: t.integer().gte(10).unique(),
}));
}
const db = createDb(
{
...dbOptions,
},
{
table: SomeTable,
},
);
const factory = ormFactory(db);
// sequence is starting from 1
// text columns are prefixed with sequence and a space:
factory.user.text; // '1 random text'
// email is prefixed with a sequence and a dash:
factory.user.email; // '1-random@email.com'
// URL is prefixed with https:// + sequence and a dash
factory.user.url; // 'https://1-random.url/'
// number is set to sequence
factory.user.number; // 1
// number with `.gt` is set to sequence + gt value
factory.user.greaterThan10; // 11
// number with `.gte` is set to sequence + gt value - 1
factory.user.greaterThan10; // 10
.max
and .length
text column methods are taken into account to not exceed the limit when prefixing the value.
omit
Omit some fields before building an object. Only for the build
method, create
will ignore it.
const partialUser = await factory.user.omit({ id: true, name: true }).build();
// partialUser has everything except id and name
pick
Pick specific fields before building an object. Only for the build
method, create
will ignore it.
const partialUser = await factory.user.pick({ id: true, name: true }).build();
// partialUser has only id and name
set
Set custom data before building or creating an object.
It takes the same argument as a build
.
const user = factory.user.set({ name: 'Vasya' }).build();
const createdUser = await factory.user.set({ name: 'Vasya' }).create();
extend
It is possible to extend a factory with custom methods:
class UserFactory extends factory.user.extend() {
specificUser(age: number) {
// can call other methods
return this.otherMethod().set({
age,
name: 'Specific name',
});
}
otherMethod() {
return this.set({ extra: true });
}
}
const userFactory = new UserFactory();
const user = userFactory.specificUser().build();
Methods can be chained:
const user = userFactory
.pick({ id: true, name: true })
.specificUser()
.set({ key: 'value' })
.build();