Mirage
Mirage is client-side server to develop, test and prototype your app. It is used for e2e tests.
Building blocks
Mirage is inspired by the ember-cli-mirage package and its approach. ember-cli-mirage docs useful to understand the internals of the BigTest Mirage and building blocks like
Route handlers. Under the hood it uses Pretender which is a mock server library for XMLHttpRequest and Fetch APIs. In the example below the server will mock GET requests to the /api/songs endpoint and return back predefined result. Additionally there is API to mock other CRUD operations which could be found in the package documentation. Usually this stuff should be placed in /bigtest/network/config.js file.
this.get('/api/songs', request => {
return [
200,
{'content-type': 'application/javascript'},
'[{"id": 12}, {"id": 14}]'
];
});
Typically there are two options to mock the response data.
1) Return just plain object
this.get('/api/authors/current', {id: 1, name: 'Link'});
2) Specify function as the handler. The function handler you define takes two parameters, schema (your Mirage server’s ORM) and request (the Pretender request object). Consult the schema’s API for how to interact with your models (or the database directly) and Pretender’s docs for more info on the request object.
this.get('/api/events', () => {
let events = [];
for (var i = 0; i < 100; i++) {
events.push({
id: i,
value: Math.random() * 100
});
};
return events; // will be JSON.stringified by Mirage
});
Returning a Model or Collection (from schema
) lets take advantage of the serializer layer.
this.get('/api/users/:id', ({ users }, request) => {
return users.find(request.params.id);
});
This handler returns a User model, which will pass through the UserSerializer if one exists, or the ApplicationSerializer otherwise.
It is possible to return an instance of Response
to dynamically set headers and the status code:
import { Response } from '@bigtest/mirage';
this.server.put('/configuration', () => {
return new Response(422, { some: 'header' }, {
errors: [{ title: 'RM-API credentials are invalid' }]
});
});
Database. Mirage server has a database which you can be interacted with in route handlers. Typically models should be used to interact with database data, but it is possible to reach into the db directly in the event for more control.
this.delete('/packages/:id', ({ packages, resources }, request) => {
let matchingPackage = packages.find(request.params.id);
let matchingResources = resources.where({
packageId: request.params.id
});
// destroy model records from db
matchingPackage.destroy();
matchingResources.destroy();
return {};
});
Here the available API methods for db can be found.
Factories. Factories are used to seed mock database, either during development or within tests. Whenever the object is generated via a factory, it will automatically get added to the database, and thus get an autogenerated id
. Factories have attributes, and objects can be created from factory definitions using the server.create
and server.createList
methods. The name of the factory is determined by the filename. To define factory the file should be created under the /bigtest/network/factories directory. The name of the factory is determined by the filename. For the sake of data randomization a faker is good tool to use.
import { Factory, faker, trait } from '@bigtest/mirage';
export default Factory.extend({
title: () => faker.company.catchPhrase(), // here the field is a function which returns some value
isCustom: false, // here the field is just a value
barcode: () => Math.floor(Math.random() * 9000000000000) + 1000000000000,
location: () => {
return { name: faker.random.word() };
}
});
Function fields receive the sequence number as an argument, which is useful to create dynamic attributes.
It is possible to customize the objects created by factory using the afterCreate()
hook. This hook fires after the object is built (so all the attributes have been defined will be populated) but before that object is returned. It is given two arguments: the newly created object, and a reference to the server. This makes it useful if it is needed factory-created objects to be aware of the rest of the state of Mirage database, or build relationships.
afterCreate(packageObj, server) {
let provider = server.create('provider'); create here another factory and assign it as field to newly created package
packageObj.provider = provider;
packageObj.save();
}
Fixtures. Fixtures are data files that can be used to seed mock database, either during development or within tests. In general Mirage recommends using factories over fixtures, though there are times where fixtures are suitable. To add data to a database table titles, for instance, first create the file /bigtest/network/fixtures/titles.js:
import padStart from 'lodash/padStart';
let packageTitles = new Array(50).fill().map((_, i) => {
return {
id: `pkg_title_${padStart(i, 3, '0')}`,
name: `Package Title ${i + 1}`
};
});
export default [
...packageTitles
];
loadFixtures could be called from within a test to load this data into mock database.
beforeEach(function () {
this.server.loadFixtures('titles');
// At this point this.server.db.titles would be populated with fixtures from bigtest/network/fixtures/titles.js.
// Screenshot below demonstrates how it looks in the code.
return this.visit('/eholdings/providers/paged_provider', () => {
expect(ProviderShowPage.$root).to.exist;
});
});
By the time the page is displayed, the fixtures would be loaded to the database. Calling loadFixtures without arguments would instantiate all fixtures /bigtest/network/fixtures/ folder.
Serializers. Serializers are responsible for formatting route handler’s response. The application serializer (/network/serializers/application.js as example in ui-eholdings
) will apply to every response. To make specific customizations, per-model serializers could be defined (e.g. /network/serializers/title.js
). Any Model or Collection returned from a route handler will pass through the serializer layer. Highest priority will be given to a model-specific serializer, then the application serializer, then the default serializer.
import { JSONAPISerializer, camelize } from '@bigtest/mirage';
export default JSONAPISerializer.extend({
serializeIds: 'always',
keyForModel(modelName) {
return camelize(modelName);
}
});
JSONAPISerializer is predefined serializer in Mirage to simulate JSON:API compliant servers. It which inherits the basic Serializer class and customize it using the hooks documented here.
All of those exist in the BigTest Mirage and links above is a good place to get acquitted with them.
Example
A good example of BigTest and basic Mirage setup can be found at this PR to ui-checkin package. More advanced scenarios could be found ui-eholdings repo where a bunch of tests are written.
Mirage useful links:
ui-eholdings (project where Mirage is used for e2e tests)
Pact useful links:
Pact JS - pact support for Javascript
Pact JVM - pact support for Java