Mirage JS Deep Dive: Understanding Factories, Fixtures And Serializers (Part 2)

About The Author

Kelvin is an independent software maker currently building Sailscasts — a platform to learn server-side JavaScript. He is also a technical writer and … More about Kelvin ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

In this second part of the Mirage JS Deep Dive series, we will be looking at Mirage JS’ Factories, Fixtures, and Serializers. We’ll see how they enable rapid API mocking using Mirage.

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
    The RestSerializer 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.

  • Part 1: Understanding Mirage JS Models And Associations
  • Part 2: Understanding Factories, Fixtures And Serializers
  • Part 3: Understanding Timing, Response And Passthrough
  • Part 4: Using Mirage JS And Cypress For UI Testing
Smashing Editorial (ra, il)