If you have a moderate to large web app and are using JSON files to store your test fixtures, a test fixture library can solve several issues you may be experiencing.
When you load the json file into a test file, you have a single instance of that test data for all of the tests in that file. If you need to change a field in your test data, all subsequent tests will run with that changed value. This can lead to confusing test failures.
Sometimes, when you need to vary the data for a test, it's tempting to create a new object with the information needed just for that single test. This leads to a proliferation of unstructured test data that can be difficult and time consuming to update when data structures change.
npm install efate
Install any extensions you want to use, two are currently available from this repo:
efate provides a typesafe way to define and create your fixtures. You can create the fixtures for each test, each with unique but understandable values. You can also set specific values of fields of interest for the current test.
There are two phase to fixture usage:
- Fixture definition
- Object creation
Import defineFixture
to create a standard fixture (without any extensions).
import {defineFixture} from "efate";
Using this function, you can define what fields should be populated and how.
const userFixture = defineFixture<User>(t => {
t.id.asNumber(); // id field will be numberical value that increments as you create user objects
t.firstName.asString();
t.lastName.asString();
})
export {userFixture};
Import the test fixture into your file and create the mock object, overriding any values that you need to for the test.
import {userFixture} from '<your fixtures>';
// generatate a fixture with system generated values
const user = userFixture.create();
// {id: 1, firstName: 'firstName1', lastName: 'lastName1'}
As you create more fixtures, the values will vary by incrementing the number for each field
// generatate a fixture with system generated values
const user = userFixture.create();
// {id: 1, firstName: 'firstName1', lastName: 'lastName1'
const user = userFixture.create();
// {id: 2, firstName: 'firstName2', lastName: 'lastName2'
You can override specific fields of the test fixture to test specific cases in your tests and still have complete objects
const user = userFixture.create({firstName: 'George', lastName: 'Washington'});
// {firstName: 'George', lastName: 'Washington'}
If you need to make more complicated changes to the fixture, you can pass a function
const user = UserFixture.create((user) => {
// make changes to the user fixture then return back the user
user.firstName = 'George';
});
// {firstName: 'George', lastName: 'lastName1'
Sometimes you need to create an object that doesn't have all fields. You can use the omit
function on fixture to specify if any fields on the type should be ommitted.
// a Order object that is store in the database
interface UserModel {
id: number,
firstName: string;
lastName: string;
dateCreated: Date;
dateUpdated: Date;
}
If you want to create fixture that would represent the object to be saved (without id, dateCreated/updated)
const userInput = userFixture
.omit('id', 'dateCreated', 'dateUpdated')
.create();
// {firstName: 'firstName1', lastName: 'lastName1'}
This function is useful when you need to omit fields on a one-off basis. You can also create a fixture with the Omit
utility type and not define builders for the omitted fields.
const UserInputFixture = defineFixture<Omit<UserModel,'id', 'dateCreated', 'dateUpdated'>>(t => {
firstName: string;
lastName: string;
})
const userInput = UserInputFixture.create();
// {firstName: 'firstName1', lastName: 'lastName1'}
You can generate an array of fixtures by using the UserFixture.createArrayWith
. The function accepts an override parameter that will either:
- single
Partial
of the object that will override all objects in the array - a subset array that will override the matching index in the return array
- a function that allows you to determine how the array will be overridden
- no value and all arrays are created with default values
Override All elements
const users = UserFixture.createArrayWith(2, {firstName: 'George'});
[
{firstName:'George', lastName: 'lastName1', ...},
{firstName:'George', lastName: 'lastName2', ...}
]
Override just the first element
const users = UserFixture.createArrayWith(2, [{firstName: 'George'}]);
[
{firstName:'George', lastName: 'lastName1', ...},
{firstName:'George', lastName: 'lastName2', ...}
]
Override with a function
const users = UserFixture.createArrayWith(2, (idx, create) => {
if (idx === 0){
return create({firstName: 'George'});
} else {
return create();
}
});
// results
[
{firstName:'George', lastName: 'lastName1', ...},
{firstName:'firstName2', lastName: 'lastName2', ...}
]
The function passed to the createFixture
allows you to determine how each field will be generated when you create a new fixture
The type specified will be user as the return type when you create
the fixture.
These two features together allow for the type-safe creation of fixtures, plus great auto-complete in your editor.
const userFixture = createFixture<User>(t => {
t.firstName.asString();
t.lastName.asString();
t.dateOfBirth.asDate();
})
If you have a type that extends another type, or you have a common set of fields used in multiple objects, you can extend a fixture so you can reuse an existing fixture definition to add fields to another fixture.
You can extend from multiple fixtures as well.
import {defineFixture} from "efate";
interface User {
name: string;
}
interface UserSession {
sessionId: string
}
interface LoggedInUser extends User, UserSession {
lastLoginDate: Date
}
const UserFixture = defineFixture<User>(t => {
t.name.asString();
})
const UserSessionFixture = defineFixture<UserSession>(t => {
t.sessionId.asString();
})
//use UserFixture to add fields to LoggedInuser fixture
const LoggedInUserFixture = defineFixture<LoggedInUser>(t =>{
// extend will accept mutliple fixtures
t.extend(UserFixture, UserSessionFixture);
t.lastLoginDate.asDate();
})
All of the type generators behavior is described in the generated Spec file.
-
withValues() uses specified text to generate values
const userFixture = createFixture<{foo:string}>(t => { t.foo.withValue('bar') }); const user = userFixture.create(); // {foo: 'bar1'}
-
asConstant() does not increment the value of the field for all fixtures
-
as((increment)=> val) custom function to generate values. The
increment
parameter is the count of usages of the fixturenew Fixture('email'.as(increment => `email${increment}@company.com`); // {email: '[email protected]'}```
-
asNumber() generates auto incrementing number values for the field
-
asDate({incrementDay: boolean}) generates a date value for the field. If
incrementDay
is true the day will increment for each fixture created. Otherwise the same date is used for all fixtures. -
asBoolean() generates a boolean value for the field
-
asArray(length = 1) generates an array for the field. The
length
parameter specifies length of the array, defaulted to 1const fixture = createFixture<{role:string[]}>(t => { t.role.asArray() }) // {roles: ['roles1']}
-
pickFrom([possible values]) randomly picks one of the possible values provided to set the field value.
const fixture = createFixture<{role:string[]}>(t => { t.role.pickFrom(['user', 'admin']) }) //will generate {role: 'user'|'admin'}
- fromFixture(Fixture) When you need to nest an object created from another fixture
const userFixture = createFixture<User>(t => { ... }); const OrderFixture = createFixture<Order>(t => { t.orderId.asNumber(); t.customer.fromFixture(UserFixture) ); // {orderId: 1, customer: {username: 'username1', email: 'email1'}}```
- fromFixture(Fixture) When you need to nest an object created from another fixture
-
arrayOfFixture(Fixture, length = 3) generates and array of object created from the fixture passed in.
-
asEmail() creates an email address
[email protected]
-
asFirstName() uses value from list of possible names to create more realistic data
-
asLastName() uses value from list of possible names to create more realistic data
-
asFullName() combines first name and last name values
-
asLoremIpsum({minLength: 10, maxLength: 25}) generates ispsum lorem text for generating longer text. Useful when using this tool for seed data.
If you have any trouble with how your fixtures are being generated, efate uses the Debug library. You can turn on debug statements with the following environment variable
DEBUG=efate:
There are several extensions available to use with efate that expand the library with new field definitions.
- efate-uuid: creates unique identifiers using
- efate-faker: use the faker library to generate field values
To use an extension, you must use the defineFixtureFactory
method to include any of the extension you plan to use as part of the defineFixture
function.
Extensions have two parts:
- An interface that describes usage
- A function that handles the implementation for the extension
These are passed to the defineFixture
function; the interface as a type parameter and the function as a function parameter.
import {defineFixtureFactory} from 'efate';
import {UUIDExtension, uuidExtension} from 'efate-uuid';
const defineFixture = defineFixtureFactory<UUIDExtension>(uuidExtension)
const userFixture = defineFixture(t => {
t.id.asUUID();
})
You can extend the efate with your own custom type builder. An example is the efate-uuid builder that creates valid UUID values
There are 2 parts to extending efate:
- Define an interface that describes value definition
- Create a function that will be used during fixture creation
When you call createFixtureFactory
, you can pass an interface as generic parameter to the function that will attach your extension to the object fields when defining a fixture. This interface takes the form of:
interface CustomDomainEmail {
asCustomDomainEmail(domainName: string): void
}
You pass an object to createFixtureFactory
that has a fieldName that matches the name of the interface
that returns a curried function with the following signature, fieldName: string, options: ?
{
asCustomDomainEmail: (fieldName: string, domainName: string) => (increment: number) => Field;
}
Example builder Function
const customDomainEmailExtension = {
asCustomDomainEmail: (fieldName: string, domainName: string = 'example.com') =>
(increment: number) => return new Field(fieldName, `email${increment}@${domainName}`);
}
You then call the factory method with this interface and object
const createFixture = createFixtureFactory<CustomDomainEmail>(customDomainEmailExtension);
If you have multiple extensions, union the interfaces and pass botth object to the factory.
const createFixture = createFixtureFactory<CustomDomainEmail & AnotherExtention>(customDomainEmailExtension, otherExtension);
These extensions will now be available when you define your fixtures.