A powerful and flexible tool for managing mock data
- Installation
- Quick Start
- Why Mocklify?
- Pipeline Overview
- Data Sources
- Filters
- Transformation Operators
- Transformation Scopes
- Terminators
- Contributors
Using NPM:
npm install -D mocklify
Using Yarn:
yarn add mocklify --dev
-
Install
mocklify
using NPM or Yarn -
(Optional) Alongside your type definitions, define a set of base mocks. Note: These only need to be defined once - we recommend colocating them with your model definitions, rather than with your tests.
export interface IUser {
id: string;
firstName: string;
lastName: string;
isAdmin: boolean;
}
export const MOCK_USERS: IUser[] = [
{
id: 'user1',
firstName: 'Harry',
lastName: 'Potter',
isAdmin: false
},
...
]
- When writing tests, use Mocklify to obtain a suitable set of mock data, manipulating it in any way that is needed for that particular test.
import { mocklify, override } from 'mocklify';
const mockData = mocklify<IUser>()
.add(10, MOCK_USERS)
.transform(
override({ isAdmin: true }),
)
.getAll();
This is just the tip of the iceberg - Mocklify offers a powerful chained API allowing data to be selected, generated, filtered and transformed in many different ways. Read on to find out more.
In short, Mocklify enables powerful, understandable and maintainable code, without many of the problems that generally occur when working with mock data.
The motivation behind the Mocklify library is explored in more depth in the "Why Mocklify?" wiki article.
To use Mocklify you define a pipeline of steps, each of which adds, removes or transforms data in some way.
At the end of the chain, you call a "terminator" function which returns the data.
Generally, the first step(s) will accumulate data from one or more sources (objects from a predefined set of mock data, newly generated mock objects, or any combination). After this, optional filtering and transformation steps can be used to manipulate the data to your needs, before it is then returned by one of the terminator functions.
The following diagram shows an example Mocklify pipeline:
const results = mocklify<IUser>()
βββββββββββββββββββββββ
β Data Sources ββ β β β β β .add(20, MOCK_USERS)
βββββββββββββββββββββββ .generate(10, greatWizards)
β
βΌ
βββββββββββββββββββββββ
β Filters ββ β β β β β .filter(isDeathEater)
βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Transformations ββ β β β β β .transform(
βββββββββββββββββββββββ omit(['isOnline']),
β where(isGryffindor,
β modify(user => user.points += 1000)
β ),
β where(isSlytherin,
β override({ points: 0 })
β ),
βΌ )
βββββββββββββββββββββββ
β Terminator ββ β β β β β .getAll();
βββββββββββββββββββββββ
Data Sources [learn more]
add
- adds a specified number of predefined mock objects to the data setaddAll
- adds all provided predefined mock objects to the data setaddOne
- adds a single predefined mock objects to the data setgenerate
- generates a specific number of new objects using a factory function, and adds them to the data setgeneratePartial
- generates a specific number of new objects using a partial factory function, and adds them to the data set
Filters [learn more]
filter
- removes any items from the data set which don't match the provided predicate
Transformations [learn more]
transform
- applies a chain of transformation operators to the data set. Transform operators apply to all items by default, but can be limited to a specific subset using transformation scopes.
Terminators [learn more]
getAll
- returns all items in the data setget
- returns the specified number of items from the data setgetSlice
- returns a subset of items in the data setgetFirst
- returns the first item in the data setgetLast
- returns the last item in the data setgetOne
- returns a single item from the data set that matches a predicate (if multiple items match, the first is returned)getWhere
- returns any items from the data set which match a predicategetRandom
- returns a specific number of random items from the data setgetRandomOne
- Returns a single item from the data set, selected at randomgetShuffled
- returns all items from the data set, shuffled into a random order
The core state of the Mocklify pipeline is an in-memory set of mock data, in the form of a strongly typed array. Because each use-case is likely to need a different set of data, Mocklify is flexible about how this data set is constructed.
At any point in the pipeline (typically the start), items can be added to current data set using any combination of:
- predefined mock objects (using the
add
,addAll
oraddOne
functions) - generated mock objects (using the
generate
function)
add(targetLength: number, items: T[], predicate?: FilterPredicate<T>)
The add
method pushes a specific number of items from a source set of predefined mock objects into the current data set.
The targetLength
parameter defines exactly how many items should be added. If the source array contains more items than requested, Mocklify takes the first n
items. If it contains less items than requested, then it will repeat items as many times are needed to reach the target length.
The optional predicate
parameter allows more control over which items from the source set are added. Only items that match the predicate will be used.
addAll(items: T[], predicate?: FilterPredicate<T>)
The addAll
method pushes all items from a source set of predefined mock objects into the current data set. This is similar to add()
but is not constrained to a specific length.
The optional predicate
parameter limits which items are included.
addOne(item: T)
The addOne
method inserts a single predefined mock object into the current data set. This is eqivalent to addAll([item])
.
generate(count: number, factory: MockFactory<T>)
The generate
method generates a specific number of new objects using the provided factory function. This is useful if you need mock objects that are not based on a predefined set of examples.
The function takes two parameters:
- The number of items to generate
- A factory function which, given the index, returns a new mock object.
export const MOCK_USER_FACTORY = (index: number): IUser => {
return {
id: `user_${index}`,
firstName: 'FirstName',
lastName: 'LastName',
isAdmin: false,
...
};
};
const twentyGeneratedUsers = mocklify<IUser>()
.generate(20, MOCK_USER_FACTORY)
.getAll();
The above example will generate 20 users with incrementing IDs, but they will all have the same static firstName
and lastName
properties.
For more sophisticated data generation, it is easy to combine Mocklify with your own functions (or other libraries) that can generate test data, like so:
import { mocklify } from 'mocklify';
import { randomFirstName, randomLastName, randomParagraph, randomUuid, } from './fake-data-utils';
// Imagine you have `randomFirstName`, `randomLastName`, `randomParagraph` and `randomUuid` functions available in `fake-data-utils.ts`...
export const MOCK_USER_FACTORY: MockFactory<IUser> = (index: number): IUser => {
return {
id: randomUuid(),
firstName: randomFirstName(),
lastName: randomLastName(),
note: randomParagraph(),
...
};
};
const twentyGeneratedUsers = mocklify<IUser>()
.generate(20, MOCK_USER_FACTORY)
.getAll();
generatePartial(count: number, factory: PartialMockFactory<T>)
The generatePartial
method is similar to generate(), except that the factory function is not required to specify all required properties of the generated object up front (i.e. it returns Partial<T>
instead of T
).
This simplies the process of creating mock objects and is particularly useful when combined with transformation operators.
In the example below, the factory function only sets up minimal information about the user (the id
property) on the assumption that any other properties of importance will be populated later in the pipeline (by transformation operators):
export const PARTIAL_MOCK_USER_FACTORY: PartialMockFactory<IUser> = (index: number): Partial<IUser> => {
return {
id: `user_${index}`
};
};
const results = mocklify<IUser>()
.generatePartial(3, PARTIAL_MOCK_USER_FACTORY)
.transform(
modify((user, index) => user.firstName = `Generated User ${index + 1}`)
)
.getAll();
After adding data from one or more data sources, filters allow the current data set to be reduced if required.
Note: These filters apply to the pipeline's entire internal data set (i.e. after combining data from one or more data sources). In many cases, it may be better to avoid adding certain items in the first place - this can be achieved by passing a predicate to the data source methods at the point of adding data.
filter(predicate: FilterPredicate)
Filters the current data set to only include items that fulfil the specified criteria.
const whereNameIsPotter: FilterPredicate<IUser> = user => user.lastName === 'Potter';
const results = mocklify<IUser>()
.addAll(MOCK_USERS)
.filter(whereNameIsPotter)
.getAll();
Transformation operators are the heart of Mocklify. They are composable, chainable methods which transform data items in some way.
To use them, pass one or more operators into the transform
pipeline step.
Mocklify's transformation operators will automatically be immutable (they will never mutate the original input sources). Internally, Mocklify uses Immer to get immutable state.
omit<T>(propsToOmit: Array<keyof T>)
Removes one or more specific properties from the data
const results = mocklify<IUser>()
.addAll(MOCK_USERS)
.transform(
omit(['isAdmin'])
)
.getAll();
override<T, P extends Partial<T>>(propsToOverride: P)
Overrides specific properties with new values, spreading them on top of the original object
const results = mocklify<IUser>()
.addAll(MOCK_USERS)
.transform(
override({ isAdmin: true }),
)
.getAll();
modify<T>(modifierFunction: ModifierFunction<T>)
Takes a callback function which gives full control over modifying the object in any way.
The provided function is given the item, its index and the full array of items. This can be very helpful for updating values based on the existing data or the index.
Mocklify takes care of applying the changes in an immutable way.
const results = mocklify<IUser>()
.addAll(MOCK_USERS)
.transform(
modify((user, index, allUsers) => {
user.id = `user_${index}`;
user.firstName = `${user.firstName} (user ${index + 1} of ${allUsers.length})`;
user.points *= 2;
})
)
.getAll();
Note: The modify
operator is a powerhouse and could, in theory, negate the need for the other operators. For example, modify
can achieve everything that override
does and more. However, we believe that the other operators bring increased readability for simple use cases.
By default, transformations apply to all items in the data set.
Sometimes, it may be useful to apply transformations only to certain items in the data set. This is possible by wrapping transformation operators
in a scope
.
where<T>(limiter: Limiter<T>, ...operators: Operator<T>[])
The where
scope applies the specified chain of transformation operators
only to those items that fulfil the criteria defined by the limiter
.
This unlocks a lot of power. For inspiration, here are some examples of how we might use scopes and operators for a list of users:
- "Promote all users in a particular group to be admins"
- "Omit the
lastName
property for a random subset of users" - "Set
isOnline
to true for the first 10 users, and false for the rest"
Let's try an example: say I'm not interested in the users' online status, I want to bump the house points of Gryffindors, nuke the points for Slytherins, max out Harry's points and make him an admin for good measure.
This could achieved as follows:
import { mocklify, modify, omit, override, where, Limiter } from 'mocklify';
const isGryffindor: Limiter<IUser> = user => user.tagIds.includes(MOCK_TAGS.gryffindor.id);
const isSlytherin: Limiter<IUser> = user => user.tagIds.includes(MOCK_TAGS.slytherin.id);
const isHarry: Limiter<IUser> = user => user.firstName === 'Harry' && user.lastName === 'Potter';
const results = mocklify<IUser>()
.addAll(MOCK_USERS)
.transform(
omit(['isOnline']),
where(isGryffindor,
modify(user => user.points += 1000)
),
where(isSlytherin,
override({ points: 0 })
),
where(isHarry,
override({
points: 9999,
isAdmin: true
})
)
)
.getAll();
Terminator methods are the final step in a Mocklify chain, used to access the fruit of your labour.
Terminator methods return the current data set, or some subset of that data set, ready for consumption by your app or tests.
getAll(): T[]
Returns all items in the data set.
get(count: number): T[]
Returns the specified number of items from the data set.
getSlice(start: number, end?: number): T[]
Returns a subset of items in the data set.
As per Array.prototype.slice
, the end
parameter is optional and negative values are supported, acting as an offset from the end of the array.
getFirst(): T | undefined
Returns the first item in the data set.
getLast(): T | undefined
Returns the last item in the data set.
getOne(predicate: FilterPredicate<T>): T | undefined
Returns a single item from the data set that matches a predicate (if multiple items match, the first is returned).
getWhere(predicate: FilterPredicate<T>): T[]
Returns any items from the data set which match a predicate.
getRandom(count: number): T[]
Returns a specific number of random items from the data set.
getRandomOne(): T | undefined
Returns a single item from the data set, selected at random (or undefined if there are no items in the data set).
getShuffled(): T[]
Returns all items from the data set, shuffled into a random order
Thanks goes to these wonderful people (emoji key):
mattwilson1024 π» π€ π π§ |
MatthewAlner π» π€ π π§ |
This project follows the all-contributors specification. Contributions of any kind welcome!