27
Why Blueprints in Ember Are Cool and How They Save You Time Writing Tests
Developing and maintaining a growing front-end code base is complex on its own. And only sufficient test coverage will allow you to continue to build features with confidence and without the fear of critical regressions.
Therefore, automatic testing is an important part of your and your team's joint efforts to build web applications, including your Ember apps.
Still, writing tests can be time-consuming.
Despite powerful testing tools available in the JavaScript and Ember ecosystem today, and even considering that the framework already provides you a great foundation for testing, you might still need to spend some time defining your own project-specific test setup.
You may have written custom stubs and mocks for those services, network requests and third-party libraries that can't be easily simulated during your testing scenarios otherwise. And as your application grows, often the number of custom testing helpers you need to include in many, if not all, of your test files increases as well.
This in turn introduces a new entry barrier for developers who are new to the code base and who want to write their first test: Due to a lack of familiarity with all the project-specific configurations, they might either spend a lot of time trying to figure out what kind of setup to copy-paste from existing test files of your test suite into their own.
Or due to a lack of time, they might just avoid writing that test altogether.
Still, writing tests is important and should require you as little on-ramp time as possible - even as your application grows.
What if you could automate the project-specific setup of your tests, so you can focus on the actual work of writing the testing scenarios for your features instead of worrying about how to setup the test to begin with?
Fortunately, Ember CLI has you covered with tools to do exactly that.
But first, let's take a look into how test scaffolding in in Ember apps works in general, and secondly, how we can modify the process for our testing needs.
Out of the box, the Ember CLI already provides you with several generate
commands to get started with writing your tests.
This is how you would start off writing an application test for my-feature
:
# create a pre-configured application test file for 'my-feature'
ember generate acceptance-test my-feature
Running this command in your shell will provide you with the following setup:
// tests/acceptance/my-feature-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
module('Acceptance | my feature', function(hooks) {
setupApplicationTest(hooks);
test('visiting /my-feature', async function(assert) {
await visit('/my-feature');
assert.equal(currentURL(), '/my-feature');
});
});
Check out the official Ember CLI Guides for more information on how to generate and use blueprints.
In the next step, you would usually start to modify the test file with your own custom testing setup, e.g. by invoking test utils you have written yourself or by importing methods from other testing libraries you frequently use.
Luckily, you don't need to do all of this work by hand.
Instead, you can automate this modification process and instruct the cli's generate
command to create a custom test file, instead of the default one.
This is where Ember CLI blueprints come in.
Anytime you run ember generate acceptance-test xyz
, the cli will create your test file based on your command-line input and the framework blueprint that is associated with the acceptance-test
parameter:
// blueprints/acceptance-test/qunit-rfc-232-files/tests/acceptance/__name__-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
module('<%= friendlyTestName %>', function(hooks) {
setupApplicationTest(hooks);
test('visiting /<%= dasherizedModuleName %>', async function(assert) {
await visit('/<%= dasherizedModuleName %>');
assert.equal(currentURL(), '/<%= dasherizedModuleName %>');
});
});
The configuration of the process itself, e.g. which naming convention to use to name your test modules, is done in the blueprint's index.js
:
// blueprints/acceptance-test/index.js
'use strict';
const fs = require('fs');
const path = require('path');
const pathUtil = require('ember-cli-path-utils');
const stringUtils = require('ember-cli-string-utils');
const useTestFrameworkDetector = require('../test-framework-detector');
module.exports = useTestFrameworkDetector({
description: 'Generates an acceptance test for a feature.',
locals: function (options) {
let testFolderRoot = stringUtils.dasherize(options.project.name());
if (options.project.isEmberCLIAddon()) {
testFolderRoot = pathUtil.getRelativeParentPath(options.entity.name, -1, false);
}
let destroyAppExists = fs.existsSync(
path.join(this.project.root, '/tests/helpers/destroy-app.js')
);
let friendlyTestName = [
'Acceptance',
stringUtils.dasherize(options.entity.name).replace(/[-]/g, ' '),
].join(' | ');
return {
testFolderRoot: testFolderRoot,
friendlyTestName,
destroyAppExists,
};
},
});
And even better: You can also customize the default scaffolding by overriding existing blueprints.
By generating your own acceptance-test
blueprint in your project, you can now extend this functionality with your own, custom acceptance test setup in a single generate
command.
To get started, you can use ember generate
while in your Ember application's directory again:
ember generate blueprint acceptance-test
This should leave you with a new blueprint configuration file at blueprints/acceptance-test-index.js
:
'use strict';
module.exports = {
description: ''
// locals(options) {
// // Return custom template variables here.
// return {
// foo: options.entity.options.foo
// };
// }
// afterInstall(options) {
// // Perform extra work here.
// }
};
To create your own modified version of acceptance test blueprints, you can lend most of the framework's default setup for your specific test setup.
For a recent app, which uses QUnit and the latest Ember QUnit API, your index.js
could look like this:
// blueprints/acceptance-test/index.js
'use strict';
const fs = require('fs');
const path = require('path');
const stringUtils = require('ember-cli-string-utils');
module.exports = {
description: 'Generates an acceptance test for a feature.',
locals: function (options) {
let destroyAppExists = fs.existsSync(
path.join(this.project.root, '/tests/helpers/destroy-app.js')
);
let friendlyTestName = [
'Acceptance',
stringUtils.dasherize(options.entity.name).replace(/[-]/g, ' '),
].join(' | ');
return {
testFolderRoot: 'tests/acceptance/',
friendlyTestName,
destroyAppExists,
};
},
};
Next, copy over the directory structure from the list of framework blueprints for your particular test setup into the a new blueprints/acceptance-test/files
directory in your project, including the default template __name__-test.js
.
We assume you're working with a recent Ember app using the latest Ember QUnit API in this example:
# copy from framework blueprints file layout...
-- qunit-rfc-232-files
|-- tests
|-- acceptance
|-- __name__-test.js
# ...to your project's file layout
-- files
|-- tests
|-- acceptance
|-- __name__-test.js
// blueprints/acceptance-test/files/tests/acceptance/__name__-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
module('<%= friendlyTestName %>', function(hooks) {
setupApplicationTest(hooks);
test('visiting /<%= dasherizedModuleName %>', async function(assert) {
await visit('/<%= dasherizedModuleName %>');
assert.equal(currentURL(), '/<%= dasherizedModuleName %>');
});
});
Now you can modify the template at blueprints/acceptance-test/files/tests/acceptance/__name__-test.js
to your needs.
In this example, we want to ensure that in each new acceptance test file generated in the future, an additional helper util from our project is imported, that ember-sinon-qunit is setup correctly and - most importantly - that our module description sparkles ✨:
// blueprints/acceptance-test/files/tests/acceptance/__name__-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { sinon } from 'sinon';
import { setupStripe } from 'my-ember-app/tests/helpers/stripe-mock';
module('<%= friendlyTestName %> ✨✨✨', function(hooks) {
setupApplicationTest(hooks);
setupStripe(hooks);
hooks.beforeEach(function() {
this.testStub = sinon.stub();
});
test('visiting /<%= dasherizedModuleName %>', async function(assert) {
await visit('/<%= dasherizedModuleName %>');
assert.equal(currentURL(), '/<%= dasherizedModuleName %>');
});
});
Finally, if we run the generate
command for creating an acceptance test again, the cli will use our custom testing blueprint configuration and modify our test file accordingly. Check it out:
ember generate acceptance-test my-feature
// tests/acceptance/my-feature-test.js
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { sinon } from 'sinon';
import { setupStripe } from 'my-ember-app/tests/helpers/stripe-mock';
module('Acceptance | my feature ✨✨✨', function(hooks) {
setupApplicationTest(hooks);
setupStripe(hooks);
hooks.beforeEach(function() {
this.testStub = sinon.stub();
});
test('visiting /my-feature', async function(assert) {
await visit('/my-feature');
assert.equal(currentURL(), '/my-feature');
});
});
Now you're all set to start testing with one single command!
You can save yourself and your team time by automating the setup of your test files leveraging blueprints in Ember.
Blueprints allow you to override the templates for existing testing generate
commands, such as ember generate acceptance-test
or ember generate component-test
.
Beyond the scope of testing and which generate
commands the framework already offers, you can add your own generate
commands, too. If you always wanted to make it easier to write documentation for your project, why not create a ember generate docs
blueprint today?
Jessy is a Senior Frontend Engineer at Meroxa, a public speaker and an organizer of the EmberJS Berlin meetup. Also, if you're into design, devops or data engineering, come join us!
27