Mirage JS Deep Dive: Understanding Factories, Fixtures And Serializers (Part 2)
Mirage JS Deep Dive: Understanding Factories, Fixtures And Serializers (Part 2)
Kelvin Omereshone 2020-05-29T11:00:00+00:00
2020-05-29T12:38:34+00:00
In the previous article of this series, we understudied Models and Associations as they relate to Mirage. I explained that Models allow us to create dynamic mock data that Mirage would serve to our application when it makes a request to our mock endpoints. In this article, we will look at three other Mirage features that allow for even more rapid API mocking. Let’s dive right in!
Note: I highly recommend reading my first two articles if you haven’t to get a really solid handle on what would be discussed here. You could however still follow along and reference the previous articles when necessary.
Factories
In a previous article, I explained how Mirage JS is used to mock backend API, now let’s assume we are mocking a product resource in Mirage. To achieve this, we would create a route handler which will be responsible for intercepting requests to a particular endpoint, and in this case, the endpoint is api/products
. The route handler we create will return all products. Below is the code to achieve this in Mirage:
import { Server, Model } from 'miragejs';
new Server({
models: {
product: Model,
},
routes() {
this.namespace = "api";
this.get('products', (schema, request) => {
return schema.products.all()
})
}
});
},
The output of the above would be:
{
"products": []
}
We see from the output above that the product resource is empty. This is however expected as we haven’t created any records yet.
Pro Tip: Mirage provides shorthand needed for conventional API endpoints. So the route handler above could also be as short as: this.get('/products')
.
Let’s create records of the product
model to be stored in Mirage database using the seeds
method on our Server
instance:
seeds(server) {
server.create('product', { name: 'Gemini Jacket' })
server.create('product', { name: 'Hansel Jeans' })
},
The output:
{
"products": [
{
"name": "Gemini Jacket",
"id": "1"
},
{
"name": "Hansel Jeans",
"id": "2"
}
]
}
As you can see above, when our frontend application makes a request to /api/products
, it will get back a collection of products as defined in the seeds
method.
Using the seeds
method to seed Mirage’s database is a step from having to manually create each entry as an object. However, it wouldn’t be practical to create 1000(or a million) new product records using the above pattern. Hence the need for factories.
Factories Explained
Factories are a faster way to create new database records. They allow us to quickly create multiple records of a particular model with variations to be stored in the Mirage JS database.
Factories are also objects that make it easy to generate realistic-looking data without having to seed those data individually. Factories are more of recipes or blueprints for creating records off models.
Creating A Factory
Let’s examine a Factory by creating one. The factory we would create will be used as a blueprint for creating new products in our Mirage JS database.
import { Factory } from 'miragejs'
new Server({
// including the model definition for a better understanding of what's going on
models: {
product: Model
},
factories: {
product: Factory.extend({})
}
})
From the above, you’d see we added a factories
property to our Server
instance and define another property inside it that by convention is of the same name as the model we want to create a factory for, in this case, that model is the product
model. The above snippet depicts the pattern you would follow when creating factories in Mirage JS.
Although we have a factory for the product
model, we really haven’t added properties to it. The properties of a factory can be simple types like strings, booleans or numbers, or functions that return dynamic data as we would see in the full implementation of our new product factory below:
import { Server, Model, Factory } from 'miragejs'
new Server({
models: {
product: Model
},
factories: {
product: Factory.extend({
name(i) {
// i is the index of the record which will be auto incremented by Mirage JS
return `Awesome Product ${i}`; // Awesome Product 1, Awesome Product 2, etc.
},
price() {
let minPrice = 20;
let maxPrice = 2000;
let randomPrice =
Math.floor(Math.random() * (maxPrice - minPrice + 1)) + minPrice;
return `$ ${randomPrice}`;
},
category() {
let categories = [
'Electronics',
'Computing',
'Fashion',
'Gaming',
'Baby Products',
];
let randomCategoryIndex = Math.floor(
Math.random() * categories.length
);
let randomCategory = categories[randomCategoryIndex];
return randomCategory;
},
rating() {
let minRating = 0
let maxRating = 5
return Math.floor(Math.random() * (maxRating - minRating + 1)) + minRating;
},
}),
},
})
In the above code snippet, we are specifying some javascript logic via Math.random
to create dynamic data each time the factory is used to create a new product record. This shows the strength and flexibility of Factories.
Let’s create a product utilizing the factory we defined above. To do that, we call server.create
and pass in the model name (product
) as a string. Mirage will then create a new record of a product using the product factory we defined. The code you need in order to do that is the following:
new Server({
seeds(server) {
server.create("product")
}
})
Pro Tip: You can run console.log(server.db.dump())
to see the records in Mirage’s database.
A new record similar to the one below was created and stored in the Mirage database.
{
"products": [
{
"rating": 3,
"category": "Computing",
"price": "$739",
"name": "Awesome Product 0",
"id": "1"
}
]
}
Overriding factories
We can override some or more of the values provided by a factory by explicitly passing them in like so:
server.create("product", {name: "Yet Another Product", rating: 5, category: "Fashion" })
The resulting record would be similar to:
{
"products": [
{
"rating": 5,
"category": "Fashion",
"price": "$782",
"name": "Yet Another Product",
"id": "1"
}
]
}
createList
With a factory in place, we can use another method on the server object called createList
. This method allows for the creation of multiple records of a particular model by passing in the model name and the number of records you want to be created. Below is it’s usage:
server.createList("product", 10)
Or
server.createList("product", 1000)
As you’ll observe, the createList
method above takes two arguments: the model name as a string and a non-zero positive integer representing the number of records to create. So from the above, we just created 500 records of products! This pattern is useful for UI testing as you’ll see in a future article of this series.
Fixtures
In software testing, a test fixture or fixture is a state of a set or collection of objects that serve as a baseline for running tests. The main purpose of a fixture is to ensure that the test environment is well known in order to make results repeatable.
Mirage allows you to create fixtures and use them to seed your database with initial data.
Note: It is recommended you use factories 9 out of 10 times though as they make your mocks more maintainable.
Creating A Fixture
Let’s create a simple fixture to load data onto our database:
fixtures: {
products: [
{ id: 1, name: 'T-shirts' },
{ id: 2, name: 'Work Jeans' },
],
},
The above data is automatically loaded into the database as Mirage’s initial data. However, if you have a seeds function defined, Mirage would ignore your fixture with the assumptions that you meant for it to be overridden and instead use factories to seed your data.
Fixtures In Conjunction With Factories
Mirage makes provision for you to use Fixtures alongside Factories. You can achieve this by calling server.loadFixtures()
. For example:
fixtures: {
products: [
{ id: 1, name: "iPhone 7" },
{ id: 2, name: "Smart TV" },
{ id: 3, name: "Pressing Iron" },
],
},
seeds(server) {
// Permits both fixtures and factories to live side by side
server.loadFixtures()
server.create("product")
},
Fixture files
Ideally, you would want to create your fixtures in a separate file from server.js
and import it. For example you can create a directory called fixtures
and in it create products.js
. In products.js
add:
// <PROJECT-ROOT>/fixtures/products.js
export default [
{ id: 1, name: 'iPhone 7' },
{ id: 2, name: 'Smart TV' },
{ id: 3, name: 'Pressing Iron' },
];
Then in server.js
import and use the products fixture like so:
import products from './fixtures/products';
fixtures: {
products,
},
I am using ES6 property shorthand in order to assign the products array imported to the products
property of the fixtures object.
It is worthy of mention that fixtures would be ignored by Mirage JS during tests except you explicitly tell it not to by using server.loadFixtures()
Factories vs. Fixtures
In my opinion, you should abstain from using fixtures except you have a particular use case where they are more suitable than factories. Fixtures tend to be more verbose while factories are quicker and involve fewer keystrokes.
Serializers
It’s important to return a JSON payload that is expected to the frontend hence serializers.
A serializer is an object that is responsible for transforming a **Model** or **Collection** that’s returned from your route handlers into a JSON payload that’s formatted the way your frontend app expects.
Mirage Docs
Let’s take this route handler for example:
this.get('products/:id', (schema, request) => {
return schema.products.find(request.params.id);
});
A Serializer is responsible for transforming the response to something like this:
{
"product": {
"rating": 0,
"category": "Baby Products",
"price": "$654",
"name": "Awesome Product 1",
"id": "2"
}
}
Mirage JS Built-in Serializers
To work with Mirage JS serializers, you’d have to choose which built-in serializer to start with. This decision would be influenced by the type of JSON your backend would eventually send to your front-end application. Mirage comes included with the following serializers:
JSONAPISerializer
This serializer follows the JSON:API spec.ActiveModelSerializer
This serializer is intended to mimic APIs that resemble Rails APIs built with the active_model_serializer gem.RestSerializer
TheRestSerializer
is Mirage JS “catch all” serializer for other common APIs.
Serializer Definition
To define a serialize, import the appropriate serializer e.g RestSerializer
from miragejs
like so:
import { Server, RestSerializer } from "miragejs"
Then in the Server
instance:
new Server({
serializers: {
application: RestSerializer,
},
})
The RestSerializer
is used by Mirage JS by default. So it’s redundant to explicitly set it. The above snippet is for exemplary purposes.
Let’s see the output of both JSONAPISerializer
and ActiveModelSerializer
on the same route handler as we defined above
JSONAPISerializer
import { Server, JSONAPISerializer } from "miragejs"
new Server({
serializers: {
application: JSONAPISerializer,
},
})
The output:
{
"data": {
"type": "products",
"id": "2",
"attributes": {
"rating": 3,
"category": "Electronics",
"price": "$1711",
"name": "Awesome Product 1"
}
}
}
ActiveModelSerializer
To see the ActiveModelSerializer at work, I would modify the declaration of category
in the products factory to:
productCategory() {
let categories = [
'Electronics',
'Computing',
'Fashion',
'Gaming',
'Baby Products',
];
let randomCategoryIndex = Math.floor(
Math.random() * categories.length
);
let randomCategory = categories[randomCategoryIndex];
return randomCategory;
},
All I did was to change the name of the property to productCategory
to show how the serializer would handle it.
Then, we define the ActiveModelSerializer
serializer like so:
import { Server, ActiveModelSerializer } from "miragejs"
new Server({
serializers: {
application: ActiveModelSerializer,
},
})
The serializer transforms the JSON returned as:
{
"rating": 2,
"product_category": "Computing",
"price": "$64",
"name": "Awesome Product 4",
"id": "5"
}
You’ll notice that productCategory
has been transformed to product_category
which conforms to the active_model_serializer gem of the Ruby ecosystem.
Customizing Serializers
Mirage provides the ability to customize a serializer. Let’s say your application requires your attribute names to be camelcased, you can override RestSerializer
to achieve that. We would be utilizing the lodash
utility library:
import { RestSerializer } from 'miragejs';
import { camelCase, upperFirst } from 'lodash';
serializers: {
application: RestSerializer.extend({
keyForAttribute(attr) {
return upperFirst(camelCase(attr));
},
}),
},
This should produce JSON of the form:
{
"Rating": 5,
"ProductCategory": "Fashion",
"Price": "$1386",
"Name": "Awesome Product 4",
"Id": "5"
}
Wrapping Up
You made it! Hopefully, you’ve got a deeper understanding of Mirage via this article and you’ve also seen how utilizing factories, fixtures, and serializers would enable you to create more production-like API mocks with Mirage. Look out for the next part of this series.
(ra, il)
From our sponsors: Mirage JS Deep Dive: Understanding Factories, Fixtures And Serializers (Part 2)