If you do not already have our events app running locally, you can clone it at the state we expect at the start of this exercise here: https://github.com/baroquon/events-app/tree/005.3.solution

In the last episode we looked at acceptance testing our Ember application. But, our acceptance tests were weak and fragile. To truly test our apps well, we need good data. That is where Ember CLI Mirage can help us. With Mirage we can intercept our app's API requests and use Pretender to generate mocks.

Let's get started. We are going to continue working on the events-app from the last couple of episodes. We'll need to do a little setup to get our events-app ready for Mirage. First, we need to create an application adapter. The application adapter will apply to all Ember models that do not have their own named adapter. For instance, an Event model will use the adapter at app/adapters/application.js unless we create an adapter at app/adapters/event.js. Let's create our application adapter. From the project root type:

$ ember g adapter application

In your new adapter add a namespace property with the value api:

import JSONAPIAdapter from 'ember-data/adapters/json-api';

export default JSONAPIAdapter.extend({
  namespace: 'api'
});

Also, change the route to call out to Ember Data for its model. In app/routes/events.js uncomment the return and delete all of the fixture data.

import Ember from 'ember';

export default Ember.Route.extend({
  model(){
    return this.store.findAll('event');
  }
});

Now, we are ready to get started with Mirage. Installing Mirage is as easy as installing any Ember addon. In your project's root directory type:

$ ember install ember-cli-mirage

You can think of Mirage as a fake API server. Just as we would set up routes on our server to respond to Ember's requests, we will set up routes in Mirage. To get started we will create Mirage versions of our Ember models. In our case we have an Address model and an Event model.

$ ember g mirage-model event && ember g mirage-model address

In the mirage models is where we will need to set up the relationships. In mirage/models/event.js:

import { Model, belongsTo } from 'ember-cli-mirage';

export default Model.extend({
  address: belongsTo()
});

And add the inverse in the Address model.

import { Model, belongsTo } from 'ember-cli-mirage';

export default Model.extend({
  event: belongsTo()
});

Now, let's create the routes to respond with these newly generated models. Right now, we'll just respond to find all and find record for addresses and events. In mirage/config.js add:

this.namespace = 'api';
this.get('/events', (schema) => {
  return schema.events.all();
});
this.get('/events/:id', (schema, request) => {
  return schema.events.find(request.params.id);
});

this.get('/addresses', (schema) => {
  return schema.addresses.all();
});
this.get('/addresses/:id', (schema, request) => {
  return schema.addresses.find(request.params.id);
});

This would be a good time to restart your Ember CLI server. Now, we need to add our factories.

$ ember g mirage-factory event && ember g mirage-factory address

In our factories we are going to use faker to help populate our database.

In mirage/factories/event.js we will add our factory details. This will map to our Ember model for the most part, but we will specify our relationships in our mirage model and implement them when we create actually populate our db. Make sure to add faker to our import from mirage.

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

export default Factory.extend({
  title() { return `${faker.hacker.ingverb()} ${faker.hacker.noun()} ${faker.hacker.noun()}`;},
  description() { return  faker.lorem.paragraph(); },
  attendeeCount() { return Math.floor((Math.random() * 30) + 1);}
});

Now, you will do the same thing in your address factory.

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

export default Factory.extend({
  street1() { return  faker.address.streetAddress(); },
  street2() { return  faker.address.secondaryAddress(); },
  city() { return  faker.address.city(); },
  state() { return  faker.address.stateAbbr(); },
  zip() { return  faker.address.zipCode(); }
});

Now we need to populate our factories. We will do this in two ways, first for our development database, then we will set it up for our tests.

For development we will load our db from mirage/scenarios/default.js. First, we'll create our addresses using server.createList. Then, we'll iterate over them and create an event for each address.

let addresses = server.createList('address', 10);

addresses.forEach((address) => {
  server.create('event', { address });
});

Here we see how to create both lists and individual objects. Now, if we were to start our server, we would see a list of ten events each with an address and populated with faker data. However, if we were to run our tests, they would fail as we have removed our fixtures and the data creates in mirage/scenarios/default.js is only loaded in development, not tests. To load them in our tests just add the same code from above at the top of the test that expects the data to be present. For example, in tests/acceptance/events-test.js our test might now look like this:

test('visiting / and clicking events takes us to the events page.', function(assert) {
  let addresses = server.createList('address', 3);

  addresses.forEach((address) => {
    server.create('event', { address });
  });

  visit('/');

  andThen(function() {
    click('a[href="/events"]');
    andThen(function() {
      assert.equal(currentURL(), '/events');
      // This is the text that is in the events/index template - it should display if we have not selected an event
      assert.equal($('article h3').text().trim(), "Choose an event on the left to see details");
      assert.equal($('aside ul li').length, 3);
    });
  });
});

That's it, we now have a access to a great client based API for our Ember test and development environments thanks to Ember CLI Mirage and the awesome folks that maintain it.

References / Notes