Part Factory is a minimalist object factory pattern with strong typing. Create object factories with suitable default value generation, then override the defaults as needed per test case.
npm install @kensio/part-factorySuppose we have an object with this structure, and we want to generate valid instances for testing:
interface Foo {
name: string;
size: number;
}The simplest object factory has static default values which can be overridden when making objects.
import { StaticFactory } from "@kensio/part-factory";
const fooFactory = new StaticFactory<Foo>({
name: "Foobar",
size: 10,
});
const defaultFoo = fooFactory.make();
// { name: "Foobar", size: 10 }
const myFoo = fooFactory.make({ size: 20 });
// { name: "Foobar", size: 20 }The types in Part Factory are detailed enough for TypeScript to enforce strong typing on the factories, object structures and overrides.
We might want to generate dynamic values for each new object. This works well with a data generation library such as Faker.
import { DynamicFactory } from "@kensio/part-factory";
import { faker } from "@faker-js/faker";
const fooFactory = new DynamicFactory<Foo>(() => ({
name: faker.word.noun(),
size: faker.number.int({ max: 100 }),
}));
const defaultFoo = fooFactory.make();
// { name: "external", size: 42 }
const myFoo = fooFactory.make({ size: 20 });
// { name: "front", size: 20 }We can create variant factories that apply preset variations to objects made by a base factory.
import { DynamicFactory, VariantFactory } from "@kensio/part-factory";
import { faker } from "@faker-js/faker";
const animalFactory = new DynamicFactory<Foo>(() => ({
name: faker.animal.type(),
size: faker.number.int({ max: 100 }),
}));
const zebraFactory = new VariantFactory<Foo>(baseFactory, {
name: "Zebra",
});
const zebra = zebraFactory.make();
// { name: "Zebra", size: 42 }
const largeZebra = zebraFactory.make({ size: 100 });
// { name: "Zebra", size: 100 }Sometimes the values you want to override are easier to represent as a structured input object, but the value you want from the factory has a different output type.
MappedFactory generates the input object, applies overrides to that input object, then maps the
completed input into the final output.
import { MappedFactory } from "@kensio/part-factory";
interface ArnComponents {
partition: "aws";
service: string;
region: string;
accountId: string;
resourceType: string;
resourceId: string;
}
type Arn = `arn:aws:${string}:${string}:${string}:${string}/${string}`;
const arnFactory = new MappedFactory<ArnComponents, Arn>(
() => ({
partition: "aws",
service: "lambda",
region: "eu-west-1",
accountId: "123456789012",
resourceType: "function",
resourceId: "example-function",
}),
(components) =>
`arn:${components.partition}:${components.service}:${components.region}:${components.accountId}:${components.resourceType}/${components.resourceId}`,
);
const defaultArn = arnFactory.make();
// "arn:aws:lambda:eu-west-1:123456789012:function/example-function"
const bucketArn = arnFactory.make({
service: "s3",
resourceType: "bucket",
resourceId: "abc123def4",
});
// "arn:aws:s3:eu-west-1:123456789012:bucket/abc123def4"The overrides are partial down through the object structure. This means that we can override one nested property without replacing the entire tree to reach that property.
import { StaticFactory } from "@kensio/part-factory";
interface UserProfile {
username: string;
contact: {
email: string;
phone: string;
address: {
line1: string;
city: string;
country: string;
};
};
}
const userProfileFactory = new StaticFactory<UserProfile>({
username: "foo_user",
contact: {
email: "foo@example.com",
phone: "123456789",
address: {
line1: "1 Foo Street",
city: "London",
country: "UK",
},
},
});
const userProfile = userProfileFactory.make({
contact: {
address: {
city: "Manchester",
},
},
});
// {
// username: "foo_user",
// contact: {
// email: "foo@example.com",
// phone: "123456789",
// address: {
// line1: "1 Foo Street",
// city: "Manchester",
// country: "UK",
// },
// },
// }