anchorWhere do we put those tests?
Before we start writing tests we need to figure out where to put all those new tests. In the case of an Ember.js app there is a top-level tests
folder, where these new tests would probably feel right at home. But inside of the folder there are only acceptance
, helpers
, integration
, and unit
subfolders. None of that really matches what we're building here so we decided to put all of our Mirage-related tests into a tests/mirage/
folder. Note that "Mirage-related" means the tests that are testing our Mirage.js setup, not the other tests that are just using the setup.
anchorTesting GET
Requests
Let's start with a simple example. We have created a user
model in Mirage.js, and a corresponding this.get('/users/:id')
shorthand route handler. Now we want to check if the serialization layer in Mirage.js works as expected. For that we will create a new test file tests/mirage/user/get-test.js
:
import { setupTest } from 'ember-qunit';
import { module, test } from 'qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import fetch from 'fetch';
module('Mirage | User', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
module('GET /users/:id', function () {
test('returns the requested user', async function (assert) {
let user = this.server.create('user', {
email: 'johnny@dee.io',
firstName: 'John',
lastName: 'Doe',
});
let response = await fetch(`/users/${user.id}`);
assert.equal(response.status, 200);
let responsePayload = await response.json();
assert.deepEqual(responsePayload, {
user: {
id: 1,
email: 'johnny@dee.io',
first_name: 'John',
last_name: 'Doe',
},
});
});
});
});
You can see here that we are first creating the test resource in the Mirage.js database (the server.create()
call), and then we use the regular fetch()
API to perform a network request and see what Mirage.js returns. We check if the correct HTTP status code is returned and then compare the resulting JSON payload with our expectation.
You may have noticed that we hardcoded the id
in this example. This works in simple cases like this, but if, for example, your API is using random UUIDs then hardcoding things like this just doesn't work. What we could use instead is: id: user.id
.
anchormatchJson()
When writing such API tests it can often happen that assert.deepEqual()
just does not provide enough flexibility. To resolve this problem we have integrated the match-json JS library into our tests, which makes assertions against nested values in the JSON payload much easier to write.
Inside the tests/test-helper.js
file we added the following snippet:
/* globals QUnit */
import match from 'match-json';
QUnit.assert.matchJson = function (actual, expected, message) {
let result = match(actual, expected);
this.pushResult({ result, actual, expected, message });
};
This imports the match-json
library, and introduces a new type of assertion on the assert
object that is available in QUnit tests. We can now write assertions like:
assert.matchJson(responsePayload, {
user: {
id: Number,
email: (value) => isEmail(value),
first_name: 'John',
last_name: 'Doe',
},
});
anchorTesting Edge Cases
One advantage of testing the Mirage.js setup is that we can make sure that edge cases also work similar to the production API. For example, if we want to ensure that our Mirage.js setup returns a "404 Not Found" HTTP status if the requested user does not exist, we can skip the server.create()
call at the start of the test, perform the fetch()
request, and then check for the expected response.status
value:
test('returns HTTP 404 if the requested user does not exist', async function (assert) {
let response = await fetch('/users/42');
assert.equal(response.status, 404);
});
anchorTesting PUT
requests
Similar to read-only GET
requests, we can also test e.g. PUT
requests, that mutate the existing resource:
module('GET /users/:id', function () {
test('returns the requested user', async function (assert) {
let user = this.server.create('user', {
email: 'johnny@dee.io',
firstName: 'John',
lastName: 'Doe',
});
let response = await fetch(`/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify({
user: {
first_name: 'Joe',
},
}),
});
assert.equal(response.status, 200);
let responsePayload = await response.json();
assert.deepEqual(responsePayload, {
user: {
id: 1,
email: 'johnny@dee.io',
first_name: 'Joe',
last_name: 'Doe',
},
});
});
});
You can see that the test looks fairly similar to the GET
request test, except that pass an options object to the fetch()
function, where we specify that this is a PUT
request, and what the request payload will be.
Similar strategies can also be applied for DELETE
and POST
requests, but we will leave that as an exercise for our readers... 😉
Finally, if you want more information on how we make sure that our mock APIs always match the production API or you need more help implementing these things in your own project, please contact us! 👋