Factories

One of the main benefits of using Mirage is the ability to quickly put your server into different states.

For example, you might be developing a feature and want to see how the UI renders for both a logged-in user and an anonymous user. This is the kind of thing that's a pain when using a real backend server, but with Mirage it's as simple as flipping a JavaScript variable and watching your app live-reload.

Factories are classes that help you organize your data-creation logic, making it easier to define different server states during development or within tests.

Let's see how they work.

Defining factories

Your first factory

Say we have a Movie model defined in Mirage.

import { createServer, Model } from "miragejs"

createServer({
  models: {
    movie: Model,
  },
})

To seed Mirage's database with some movies so you can start developing your app, use the server.create method in your server's seeds:

import { createServer, Model } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  seeds(server) {
    server.create("movie")
  },
})

server.create takes the singular hyphenated form of your model's class name as its first argument.

Because we have no Factory defined for a Movie, server.create('movie') will just create an empty record and insert it into the database:

// server.db.dump();
{
  movies: [{ id: "1" }]
}

Not a very interesting record.

However, we can pass attributes of our own as the second argument to server.create:

server.create("movie", {
  title: "Interstellar",
  releaseDate: "10/26/2014",
  genre: "Sci-Fi",
})

Now our database looks like this:

// server.db.dump()

{
  "movies": [
    {
      "id": "1",
      "title": "Interstellar",
      "releaseDate": "10/26/2014",
      "genre": "Sci-Fi"
    }
  ]
}

and we can actually start developing our UI against realistic data.

This is a great way to start, but it can be cumbersome to manually define every attribute (and relationship) when working on data-driven applications. It would be nice if we had a way to dynamically generate some of these attributes.

Fortunately, Factories let us do just that!

Let's define a Factory for our Movie model using the factories key of our server options and the Factory import:

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      // factory properties go here
    }),
  },

  seeds(server) {
    server.create("movie")
  },
})

Right now the Factory is empty. Let's define a property on it:

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title: "Movie title",
    }),
  },

  seeds(server) {
    server.create("movie")
  },
})

Now server.create('movie') will use the properties from this factory. The inserted record will look like this:

{
  "movies": [{ "id": "1", "title": "Movie title" }]
}

We can also make this property a function.

Factory.extend({
  title(i) {
    return `Movie ${i}`
  },
})

i is an incrementing index that lets us make dynamic factory attributes.

If we use the server.createList method, we can quickly generate five movies

server.createList("movie", 5)

and with the above factory definition, our database will now look like this:

{
  "movies": [
    { "id": "1", "title": "Movie 1" },
    { "id": "2", "title": "Movie 2" },
    { "id": "3", "title": "Movie 3" },
    { "id": "4", "title": "Movie 4" },
    { "id": "5", "title": "Movie 5" }
  ]
}

Let's add some more properties to our factory:

import { createServer, Model, Factory } from "miragejs"
import faker from "faker"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title(i) {
        return `Movie ${i}`
      },

      releaseDate() {
        return faker.date.past().toLocaleDateString()
      },

      genre(i) {
        let genres = ["Sci-Fi", "Drama", "Comedy"]

        return genres[i % genres.length]
      },
    }),
  },

  seeds(server) {
    // Use factories here
  },
})

Here we've installed the Faker.js library to help us generate random dates.

Now if we create 5 movies in our development seeds

seeds(server) {
  server.createList('movie', 5)
}

we'll have this data in our database:

{
  "movies": [
    {
      "id": "1",
      "title": "Movie 1",
      "releaseDate": "5/14/2018",
      "genre": "Sci-Fi"
    },
    {
      "id": "2",
      "title": "Movie 2",
      "releaseDate": "2/22/2019",
      "genre": "Drama"
    },
    {
      "id": "3",
      "title": "Movie 3",
      "releaseDate": "6/2/2018",
      "genre": "Comedy"
    },
    {
      "id": "4",
      "title": "Movie 4",
      "releaseDate": "7/29/2018",
      "genre": "Sci-Fi"
    },
    {
      "id": "5",
      "title": "Movie 5",
      "releaseDate": "6/30/2018",
      "genre": "Drama"
    }
  ]
}

As you can see, Factories let us rapidly generate different scenarios for our dynamic server data.

Attribute overrides

Factories are great for defining the "base case" of your models, but there's plenty of times where you'll want to override attributes from your factory with specific values.

The last argument to create and createList accepts a POJO of attributes that will override anything from your factory.

// Using only the base factory
server.create('movie');
// gives us this object:
{ id: '1', title: 'Movie 1', releaseDate: '01/01/2000' }

// Passing in specific values to override certain attributes
server.create('movie', { title: 'Interstellar' });
// gives us this object:
{ id: '2', title: 'Interstellar', releaseDate: '01/01/2000' }

Think of your factory attributes as a reasonable "base case" for your models, and then override them in development and testing scenarios as you have need for specific values.

Dependent attributes

Attributes can depend on other attributes via this from within a function. This can be useful for quickly generating things like usernames from names:

factories: {
  user: Factory.extend({
    name() {
      return faker.name.findName()
    },

    username() {
      return this.name.replace(" ", "").toLowerCase()
    },
  })
}

Calling server.createList('user', 3) with this factory would generate this data:

[
  { "id": "1", "name": "Retha Donnelly", "username": "rethadonnelly" },
  { "id": "2", "name": "Crystal Schaefer", "username": "crystalschaefer" },
  { "id": "3", "name": "Jerome Schoen", "username": "jeromeschoen" }
]

Relationships

In the same way that you use the ORM to create relational data with the underlying schema object

let nolan = schema.people.create({ name: "Christopher Nolan" })

schema.movies.create({
  director: nolan,
  title: "Interstellar",
})

you can also create relational data with your factories:

let nolan = server.create("director", { name: "Christopher Nolan" })

server.create("movie", {
  director: nolan,
  title: "Interstellar",
})

nolan is a model instance, which is why we can just pass it in as an attribute override when creating the Interstellar movie.

This also works when using createList:

server.create("actor", {
  movies: server.createList("movie", 3),
})

In this way you use factories to help you quickly create graphs of relational data:

server.createList("user", 5).forEach((user) => {
  server.createList("post", 10, { user }).forEach((post) => {
    server.createList("comment", 5, { post })
  })
})

This code generates 5 users, each of which has 10 posts with each post having 5 comments. Assuming these relationships are defined in your models, all the foreign keys would be set correctly in Mirage's database.

The afterCreate hook

In many cases, setting up relationships manually (as shown in the previous section) is perfectly fine. However there are times where it makes more sense to have base case relationships set up for you automatically.

This is where afterCreate hook comes in handy. It's a hook that's called after a model has been created using the factory's base attributes. This hook lets you perform additional logic on your newly-created models before they're returned from create and createList.

Let's see how it works.

Say you have these two models in your app:

import { createServer, Model, belongsTo } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },
})

Let's further suppose that in your app, it is never valid to create a post without an associated user.

You can use afterCreate to enforce this behavior:

import { createServer, Model, belongsTo, Factory } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      afterCreate(post, server) {
        post.update({
          user: server.create("user"),
        })
      },
    }),
  },
})

The first argument to afterCreate is the object that was just created (in this case the post), and the second is a reference to the Mirage server instance, so that you can invoke other factories or inspect any other server state needed to customize your newly-created object.

In this example our factory will immediately create a user for this post. That means elsewhere the your app (say, a test) you could just create a post

server.create("post")

and you'd be working with a valid record, since that post would automatically have an associated user created and associated with it.

Now, there's a one problem with the way we've implemented this so far. Our afterCreate hook updates the post's user regardless if that post already had a user associated with it.

That means that this code

let jane = server.create("user", { name: "Jane" })
server.createList("post", 10, { user: jane })

would not work as we expect, since the attribute overrides are used while the object is being created, but the logic in afterCreate runs after the post has been created. Thus, this post would be associated with the newly created user from the hook, rather than Jane.

To fix this, we can update our afterCreate hook to first check if the newly created post already has a user associated with it, and only if it doesn't will we create a new one and update the relationship:

Factory.extend({
  afterCreate(post, server) {
    if (!post.user) {
      post.update({
        user: server.create("user"),
      })
    }
  },
})

Now callers can pass in specific users

server.createList("post", 10, { user: jane })

or omit specifying a user if the details of that user aren't important

server.create("post")

and in both cases they'll end up with a valid Post record.

afterCreate can also be used to create hasMany associations, as well as apply any other relevant creation logic.

Traits

Traits are an important feature of factories that make it easy to group related attributes. Define them by importing trait and adding a new key to your factory.

For example, here we define a trait named published on our post factory:

import { createServer, Model, Factory, trait } from "miragejs"

createServer({
  models: {
    post: Model,
  },

  factories: {
    post: Factory.extend({
      title: "Lorem ipsum",

      published: trait({
        isPublished: true,
        publishedAt: "2010-01-01 10:00:00",
      }),
    }),
  },
})

You can pass anything into trait that you can into the base factory.

We can use our new trait by passing in the name of the trait as a string argument to create or createList:

server.create("post", "published")
server.createList("post", 3, "published")

The created posts will have all the base attributes, as well as everything under the published trait.

You can also compose multiple traits together. Given the following factory that has two traits defined

post: Factory.extend({
  title: "Lorem ipsum",

  published: trait({
    isPublished: true,
    publishedAt: "2010-01-01 10:00:00",
  }),

  official: trait({
    isOfficial: true,
  }),
})

we can pass our new traits into create or createList in any order:

let officialPost = server.create("post", "official")
let officialPublishedPost = server.create("post", "official", "published")

If multiple traits set the same attribute, the last trait wins.

As always, you can pass in an object of attribute overrides as the last argument, even if you're using a trait:

server.create("post", "published", { title: "My first post" })

When combined with the afterCreate() hook, traits simplify the process of setting up related object graphs.

Here we define a withComments trait that creates 3 comments for a newly created post:

post: Factory.extend({
  title: "Lorem ipsum",

  withComments: trait({
    afterCreate(post, server) {
      server.createList("comment", 3, { post })
    },
  }),
})

We can use this trait to quickly make 10 posts with 3 comments each:

server.createList("post", 10, "withComments")

Combining traits with the afterCreate hook is one of the most powerful features of Mirage factories. Effective use of this technique will dramatically simplify the process of creating different graphs of relational data for your app.

When creating an object with one or more traits, the factory will run every applicable afterCreate hook. The base factory's afterCreate hook will run first (if it exists), and any trait hooks will run in the order the traits were specified in the call to create or createList.

The association helper

The association() helper provides some sugar for creating belongsTo relationships.

As we saw earlier, the afterCreate hook lets us pre-wire relationships:

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      afterCreate(post, server) {
        if (!post.user) {
          post.update({
            user: server("user"),
          })
        }
      },
    }),
  },
})

The association() helper effectively replaces this code:

import { createServer, Model, Factory, association } from "miragejs"

createServer({
  models: {
    user: Model,

    post: Model.extend({
      user: belongsTo(),
    }),
  },

  factories: {
    post: Factory.extend({
      user: association(),
    }),
  },
})

This should help reduce some of the boilerplate in your factory definitions.

You can also use association() within traits. This definition

post: Factory.extend({
  withUser: trait({
    user: association(),
  }),
})

would let you write server.create('post', 'withUser') to create a post with an associated user.

You can also pass additional traits and overrides to association() for the related model's factory:

post: Factory.extend({
  withUser: trait({
    user: association("admin", { role: "editor" }),
  }),
})

Note that the association() helper cannot be used if your belongsTo relationship is polymorphic. Also, association() doesn't work for hasMany relationships. In both of these cases, you should continue to use the afterCreate hook to seed your data.

Using factories

In development

To use your factories to seed your development database, call server.create and server.createList in your server's seeds function:

import { createServer, Model, Factory } from "miragejs"

createServer({
  models: {
    movie: Model,
  },

  factories: {
    movie: Factory.extend({
      title(i) {
        return `Movie ${i}`
      },
    }),
  },

  seeds(server) {
    server.createList("movie", 10)
  },
})

There's no explicit API for switching scenarios in development, but you can just use JavaScript modules to split things up.

For example, you could create a new file for each scenario that contains some seeding logic

// mirage/scenarios/admin.js
export default function (server) {
  server.create("user", { isAdmin: true })
}

...export all scenarios as an object from an index.js file

// mirage/scenarios/index.js
import anonymous from "./anonymous"
import subscriber from "./subscriber"
import admin from "./admin"

export default scenarios = {
  anonymous,
  subscriber,
  admin,
}

...and then import that object into default.js.

Now you can quickly switch your development state by changing a single variable:

// mirage/server.js
import scenarios from "./scenarios"

// Choose one
const state =
  // 'anonymous'
  // 'subscriber'
  "admin"

createServer({
  // other config,

  seeds: scenarios[state],
})

This can be handy while developing your app or sharing the different states of a new feature with your team.

In testing

When you run your server in a test environment, the behavior of your server changes slightly.

createServer({
  environment: "test", // default is development

  seeds(server) {
    // This function is ignored when environment is "test"
    server.createList("movie", 10)
  },
})

In test, Mirage will load all your server config, but it will ignore your seeds. (It also sets the timing to 0 for route handlers and hides logs from the console.)

That means each test starts out with a clean database, giving you the opportunity to set up only the state needed for that test. It also keeps your development environment isolated from your tests, so that you don't inadvertently break your test suite while tweaking your seeds.

To seed Mirage's database within a test, use the server.create and server.createList methods.

For example, if you're using @testing-library/react, your test might look like this:

let server

beforeEach(() => {
  server = startMirage()
})

afterEach(() => {
  server.shutdown()
})

test("I see a message if there are no movies", () => {
  const { getByTestId } = render(<App />)
  expect(getByTestId("no-movies")).toBeInTheDocument()
})

test("I see a list of the movies from the server", async () => {
  server.createList("movie", 5)

  const { getByTestId } = render(<App />)
  await waitForElement(() => getByTestId("movie-list"))

  expect(getByTestId("movie")).toHaveLength(5)
})

In the first test, we boot up our Mirage server, but don't seed it with any movies. When we boot up the React app, we assert that there's an element in the document with a message that there were no movies found.

In the second test, we also boot up our Mirage server, but we seed it with 5 movies. This time when we render our React app, we wait for a movie-list element to be present. We use await because our React app is making a network request, which is asynchronous. Once Mirage responds to that request, we assert that those movies show up in our UI.

Each test starts out with a fresh Mirage server, so none of Mirage's state leaks across tests.

You can read more about testing with Mirage in the Testing section of these guides.

Factory best practices

In general, it's best to define a model's base factory using only the attributes and relationships that comprise the minimal valid state for that model. You can then use afterCreate and traits to define other common states that contain valid, related changes on top of the base case.

This advice goes a long way towards keeping your test suite maintainable.

If you don't use traits and afterCreate, your tests will become bogged down in irrelevant details related to setting up the data needed for that test.

test("I can see the title of a post", async function (assert) {
  let session = server.create("session")
  let user = server.create("user", { session })
  server.create("post", {
    user,
    title: "My first post",
    slug: "my-first-post",
  })

  await visit("/post/my-first-post")

  assert.dom("h1").hasText("My first post")
})

This test is only concerned with asserting the title of a post gets rendered to the screen, but it has lots of boilerplate code that's only there to get the post in a valid state.

If we used afterCreate instead, the developer writing this test could simply create a post with a specified title and slug, since those are the only details relevant to the test:

test("I can see the title of a post", async function (assert) {
  server.create("post", {
    title: "My first post",
    slug: "my-first-post",
  })

  await visit("/post/my-first-post")

  assert.dom("h1").hasText("My first post")
})

afterCreate could take care of setting up the session and user in valid states, and associating the user with the post, so that the test can stay concise and focused on what it's actually testing.

Effective use of traits and afterCreate keeps your test suite less brittle and more robust to changes in your data layer, since tests only declare the bare minimum setup logic needed to verify their assertions.


Up next, we'll take a look at how to use Fixtures as an alternative way to seed your database.