Why Blueprints in Ember Are Cool and How They Save You Time Writing Tests
Tags: ember, javascript, testing, webdev
Published: 2021-07-10 12:10:52

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](https://emberobserver.com/categories/testing) today, and even considering that the [framework already provides you a great foundation for testing](https://guides.emberjs.com/release/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.

## Easy Test Scaffolding with Generate Commands

Out of the box, the [Ember CLI](https://cli.emberjs.com/) already provides you with [several `generate` commands](https://cli.emberjs.com/release/basic-use/cli-commands/#generatemorefiles) to get started with writing your tests.

This is how you would start off writing an application test for `my-feature`:

```bash
# 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:

```javascript
// 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](https://cli.emberjs.com/release/advanced-use/blueprints/) 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.

## Blueprints Define Your Testing Scaffold Layout

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](https://github.com/emberjs/ember.js/blob/master/blueprints/acceptance-test/qunit-rfc-232-files/tests/acceptance/__name__-test.js):

```javascript
// 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`](https://github.com/emberjs/ember.js/blob/master/blueprints/acceptance-test/index.js):

```javascript
// 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.

## Writing Your Own Testing Blueprint for Your Ember App

To get started, you can use `ember generate` while in your Ember application's directory again:


```bash
ember generate blueprint acceptance-test
```

This should leave you with a new blueprint configuration file at `blueprints/acceptance-test-index.js`:

```javascript
'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](https://github.com/emberjs/ember.js/tree/master/blueprints/acceptance-test) for your specific test setup.

For a recent app, which uses QUnit and the [latest Ember QUnit API](https://github.com/emberjs/rfcs/blob/master/text/0232-simplify-qunit-testing-api.md), your `index.js` could look like this:

```javascript
// 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](https://github.com/emberjs/ember.js/tree/master/blueprints/acceptance-test) 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
```

```javascript
// 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](https://github.com/elwayman02/ember-sinon-qunit) is setup correctly and - most importantly - that our module description sparkles ✨:


```javascript
// 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:


```bash
ember generate acceptance-test my-feature
```

```javascript
// 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!

## Improve Your Developer Experience with Ember and Custom Blueprints

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](https://cli.emberjs.com/release/advanced-use/blueprints/), 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](https://meroxa.io/), a public speaker and an organizer of the [EmberJS Berlin](https://twitter.com/emberliners) meetup. Also, if you're into design, devops or data engineering, [come join us!](https://jobs.lever.co/meroxa)