Back to the front page

Testing Ember with Ember CLI Mirage

At AlphaSights, we're always trying to improve the functionality and features of our applications. One of the reasons we can iterate so quickly is our confidence in the tests we write.

Testing Ember applications generally gets a bad rap due to some unfortunate experiences. While Ember testing may not be a 100% pain-free experience, there are smart people making big strides towards an easier and better testing future. If your Ember application is one that consumes a back-end API, then Ember Mirage is an addon that you'll find extremely useful. Ember CLI Mirage provides a client-side server that we can use to test our app in isolation from our API. It provides factories to help us build models that can be used in tests and in development.

In this post we'll build a very simple app that displays a list of movies and use ember-cli-mirage to test it. I'll work off the assumption that you're using ember-cli version 0.2.7 and that you have NPM and bower installed and ready to go.

The Setup

The first step, as always, is installing the tools. Thanks to ember-cli, installing mirage is as easy as:

ember new mirage-example  
# Version: 0.2.7
# Installed packages for tooling via npm.
# installing...
# etc etc

$ cd mirage-example

$ ember install ember-cli-mirage

During installation, ember-cli-mirage created some files for us, including a configuration file for mirage, located in app/mirage/config.js. Before we dive into any of these files though, we need to piece together some routes and models for the ember application. You can check out the full application on GitHub but the main files that you need to be aware of are these ones:

router.js

Router.map(function() {  
  this.route('movies', { path: '/' });
});

adapters/application_adapter.js

import DS from 'ember-data';

export default DS.ActiveModelAdapter.extend({  
  host: 'api.my-awesome-company.com/v1'
});

models/movie.js

import DS from 'ember-data';

export default DS.Route.extend({  
  name: DS.attr('string'),
  rating: DS.attr('number'),
  trailerLink: DS.attr('string')
});

Here we used the ActiveModelAdapter, created a simple model for movies and an index route for the application. Right now, if you were to start the application using ember server and attempt to load your route, you would see this error in the console:

Mirage: Your Ember app tried to GET 'my-api.my-awesome-company.com/v1/movies', but there was no route defined to handle this request.

Define a route that matches this path in your mirage/config.js file.  

This is mirage telling us that if we intend to use it in development we need to stub out the movies route. You may remember that when ember-cli-mirage was installed it created some files for us. One of the files was a configuration file for mirage, it's located in app/mirage/config.js. While you're working in this file, it's important to note that even though it may feel like we are communicating directly with ember-data, we're not. Ember CLI Mirage maintains its own datastore to store objects and return data to the Ember application from.

Above I mentioned the use of factories. Factories in mirage are used to generate models and they're stored in mirage's datastore. Here's a simple factory for a movie:

app/mirage/factories/movie.js

import Mirage from 'ember-cli-mirage';

export default Mirage.Factory.extend({  
  name: i => `name ${i}`,
  rating: 5,
  trailer_link: i => `https://youtube.com/video/${i}`
});

app/mirage/config.js

export default function() {  
  this.namespace = 'my-api.my-awesome-company.com';

  this.get('/movies', 'movies');
}

During this step we also created the config setup for mirage. Two end points were stubbed, the main index that will return all created movies from mirage's store and a post to create movies. The first end point uses a short hand syntax. Using this syntax will tell mirage to just fetch all the movies in its store and return them.

Remember, Mirage is a replacement for our back-end server, which in this case is Rails. Because of this, we need to use camel case so our ActiveModelAdapter can correctly deserialise and populate the ember-data store.

Development

If you were to head back to the browser and refresh the page at this point, you would see the page load, but no movies would be rendered. When you're in the development environment, Mirage lets you create scenarios that can be used to seed your database. Scenarios will only be used in development and won't be loaded when you're in the test environment. Here's our default scenario:

app/mirage/scenarios/default.js

export default function(store) {  
  store.createList('movie', 10);
}

Now when you refresh the page, you'll see all the movies that were created by the factory and loaded in via the default scenario. If you were building a larger app, you could continue to create factories and add them to the scenario to be seeded when the application loads.

Testing

Let's take a look at how to use mirage to test our app. The first test will verify that when a new movie is created via the form, it's rendered on to the page. Here is the test:

import Ember from 'ember';  
import { module, test } from 'qunit';  
import testHelper from '../test-helper';

module('Acceptance: Movies', {  
  beforeEach: function() {
    testHelper.beforeEach.apply(this, arguments);
    visit('/');
  },

  afterEach: function() {
    testHelper.afterEach.apply(this, arguments);
  }
});

test('created movies are shown on the page', function(assert) {  
  visit('/');

  andThen(function() {
    fillIn(find('input.name'), 'Jurassic World');
    fillIn(find('input.rating'), '10');
    fillIn(find('input.trailer'), 'https://www.youtube.com/watch?v=RFinNxS5KN4');

    click('button.form-submit');

    andThen(function() {
      assert.equal(find(".movies-list .movie").length, 1);
    });
  });
});

You can run tests in ember by using the ember test command or you can use live reloading by running ember test --server. All we're doing here is finding each of the inputs on the form and fill them in with the data for our movie then submit the form. When the submit button is clicked, the movies controller will attempt to create a new record with ember-data which in turn will try to persist it to our Rails back-end. If you were to run this now, you might see an error from mirage explaining that the end point you are trying to hit is not stubbed. Let's do that now in the mirage config file. We'll update the file to look like this:

export default function() {  
  this.namespace = 'my-api.my-awesome-company.com';

  this.get('/movies', 'movies');

  this.post('/movies', function(store, request) {
    var attrs = JSON.parse(request.requestBody);
    var movie = store.movies.insert(attrs);
    return { movie: movie };
  });
}

When your mirage server (set up through config.js) receives a post request, you'll also get the mirage store and the originating request as arguments. We first parse the requests body in to JSON, then insert the record into mirage's store for later use. Lastly, the movie is returned to our ember application. Mirage makes zero assumptions about whether we are using ember-data or not, so we need to manipulate the response manually to ember-data-ify it.

Let's look at another type of test. Here we'll assert that the movies coming from the server are the same as the movies rendered on to the page:

test('movies are shown on the page', function(assert) {  
  andThen(function() {
    var movie = server.create('movie');

    visit('/');

    andThen(function() {
      assert.equal(find(".movies-list .movie span.title").html(), `Name: ${movie.name}`);
    });
  });
});

When you use Mirage you also get access to its store in your tests. For this, we'll create a new movie in Mirage's store before we run any tests. This will mean that when our page loads for the first time, the ember application will ping our end point looking for movies and mirage will return the movie we created in the mirage store.

If you're interested in seeing the app in full, you can find it on GitHub under our account here: https://github.com/alphasights/ember-cli-mirage-example.

We use ember and ember-cli-mirage in production applications. If you want to write ember applications that are iterated on daily and loved by many, we're hiring.

Resources

https://github.com/alphasights/ember-cli-mirage-example
http://www.ember-cli-mirage.com/
http://guides.emberjs.com/v1.10.0/testing/integration/

Comments

comments powered by Disqus