Testing Ember with Mirage

Ember is a fast, responsive, flexible front-end framework. As a front-end framework, however, we have to make some interesting decisions in both our development and testing environments.

Most of our Ember applications will make calls to an external API through which they will send and retrieve the data our users will interact with. Being dependent on an external source for data begs a few questions are we move through the development process: Do we set up and connect to an API before writing our Ember code that will present and allow the user to interact with data? Do we write tests that call to that API actually read from/write to our database every time we run our test suite?

There are a number of drawbacks to the approach of first writing and/or connecting to an API. Essentially it holds our Ember development process hostage to an external API. If you have a client who has hired you to make them a beautiful and responsive front-end app, you don't want to tell them that you can't show them any content until you finish developing or connecting with an API.

Our Ember apps don't really care where they get their data from. So long as our adapter is working and our models are set up, the meat of our application––the routes, templates and components we would build––will work just fine.

As for our test suite, it is common enough practice to stub or mock web requests in the test environment. We don't want our tests to be dependent on an external resource, or even an internet connection, and we don't want our tests to write to and change the data in our development or production database.

For these reasons it is clear that we need a way to develop our Ember applications without connecting to an external API. We need a way to seed a development database and mock or stub the web requests our app will make to it's API.

Luckily for us we can do just that with Mirage.

What is Mirage?

I'm glad you asked. Mirage is built on top of Pretender, it provides a mock server, run entirely on the client-side, that we can use both for development and testing. Mirage allow us to define routes and factories for generating seed and test data.

With mirage we can easily stub the web requests triggered by visiting our applications routes and serve fake data back in response, thereby allowing us to interact with dynamic server data without connecting our Ember app to a real live API or database.

Setting Up Mirage

Getting Mirage up and running pretty straightforward.

Installation

We can install Mirage using ember-cli. Just use the following command:

ember install ember-cli-mirage

This will generate an app/mirage folder for us which includes the following:

  • app/mirage/factories - We'll define our factories here.
  • app/mirage/scenarios/default.js - This is where we place the code that generates the seed data for our development database.
  • app/mirage/config.js - This is where we define the routes that we'll allow Mirage to stub for us.
Defining Our Factories

Factories allow us to generate records for the client-side database provided by Mirage. Mirage Factories utilize Faker.js, a library for generating fake data.

Let's take a look at an example of a factory. This particular factory is going to generate fake records for a Resource model, in an app that lists educational resources.

// app/mirage/factories/resource.js

import Mirage, {faker}  from 'ember-cli-mirage';

export default Mirage.Factory.extend({  
  title: function(){
    return `Learn ${faker.hacker.ingverb()}`;
  },
  description: function(){
    return `This is a great book! ${faker.hacker.phrase()}`;
  },
  url: faker.internet.domainName,
  topic: faker.hacker.noun
});

We create factories by creating new files in the app/mirage/factories directory. Later, when we want to reference this factory in our tests, we reference by the filename, in this case, resource.

Your factory must import Mirage and Faker, as above, then we can extend the default factory with whatever properties your desired records need. In this case, the Resource model has a title, description, url and topic. So, we set each of these properties equal to the return value of a function that uses Faker to generate random appropriate strings.

Now that our factory is ready to go, let's prepare to seed our development database.

Seeding the Development Database

In order to see our development database, we need to define a function in the app/mirage/scenarios/default.js file:

export default function(server) {  
  server.createList('resource', 10);
}

This function takes in an argument of an instance of the Mirage server and calls the createList function on that instance. createList takes in an argument of the factory you want to use to generate records and the number of records you want to generate.

This function will be invoked when the Mirage server is initialized.

Defining the Mirage Server

We need to tell Mirage what requests to mock. If our app is set up to send cross-origin requests to an external API, we need to tell Mirage to mock that cross-origin request. If your routes are namespaced, we can configure Mirage to handle that as well.

Let's say our app is connected to an external API at http://www.somefancyherokuthing.herokuapp.com and that the API is namespaced under v1.

Our adapter might look like this:

// app/mirage/config.js

import ActiveModelAdapter from 'active-model-adapter';

export default ActiveModelAdapter.extend({  
  namespace: 'v1',
  host: 'https://dry-shore-2260.herokuapp.com'
});

Our Mirage server would consequently look like this:

export default function() {  
  this.urlPrefix = 'https://dry-shore-2260.herokuapp.com'; 
  this.namespace = 'v1';  

  // define your routes here

this.urlPrefix globally configures the domain for our cross-origin API requests and this.namespace sets the namespace for those requests.

Defining the Routes

We also define the routes for our Mirage server in app/mirage/config.js. We can define our routes using the get, post, put and del.

To define a route, we use:

this.get('/resources')  

We give each route an argument of the URL we are trying to stub. Here's a set of shorthand route definitions that will mock the basic CRUD routes:

this.get('/resources');  
this.get('/resources/:id');  
this.get('/resources/new');  
this.post('/resources');  
this.del('/resources/:id');  

Using Mirage in Testing

Now that Mirage is installed and we've defined our factory, let's take a look at how we use them in our test suite.

To populate our test database using our factory, all we have to do is use the server.create and server.createList functions made available to us by Mirage.

Let's take a look at an acceptance test that expects the index page to display a list of resources:

// app/tests/acceptance/routing-test.js

describe('Acceptance: Getting Around', function() {  
  var application;

  beforeEach(function() {
    application = startApp();
  });

  afterEach(function() {
    Ember.run(application, 'destroy');
  });

  it('lists all of the resources', function(){
      server.create('resource', {title: "Hacking 101"});
      server.createList('resource', 2);

      visit('/');
      click(".resources");

      andThen(function(){
        expect(find('ul li.resources').length).to.eq(3);
        expect(find('ul li:first  a').attr('href')).to.eq("/resources/1");
      expect(find('ul li:first a').text().trim()).to.eq("Hacking 101");
    });
  });
});

In the it block, we use server.create to generate a resource record with a specific title. We give the create function an argument of the factory we want to use to generate the record and an object that contains the title property of the record we will generate.

Then we use the server.createList function to generate two additional random resources. The createList function will generate records using the instructions provided by the resource factory, while server.create, with the provided second argument of an object, will overwrite the randomly factory-generate title property with the title we specified.

Now that we have test data, the test will proceed to visit the resources index page, which will display these test records.

Learn More

This has been a very basic introduction to setting up and using Mirage. To learn more, check out the excellent and extensive Mirage documentation. One aspect that I've struggled to find good resources on is using Mirage to generate records with associations. For example a user might have many resources and resource might belong to a user. If anyone has any tips, reach out in the comments!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus