๐Ÿš€ Ember QUnit Simplification ๐Ÿ’…

๐Ÿš€ Ember QUnit Simplification ๐Ÿ’…

emberjs/rfcs#232 was merged a few months ago, and is finally ready for you to start testing out (pun intended ๐Ÿ˜œ) in your projects!

Read on for more details about why we're making this change, the details of the new API, and how we plan to help you migrate your application code.

tl;dr ๐Ÿ‘Š

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from 'ember-test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('pretty-color', function(hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function() {
    // custom setup here
  });
  
  test('renders', async function(assert) {
    assert.expect(1);

    await render(hbs`{{pretty-color name="red"}}`);

    assert.equal(
      this.element.querySelector('.color-name').textContent,
      'red'
    );
  });
});

Want these โœจawesomeโœจ looking tests? Do this in your project๐Ÿš€:

# if used, update to the latest version of linting addons
# to fix a bug in detecting beta versions of ember-cli-qunit
npm install --save-dev ember-cli-eslint
npm install --save-dev ember-cli-template-lint

# update to the beta version of ember-cli-qunit / ember-qunit
npm install --save-dev ember-cli-qunit@^4.1.0-beta.2

Report issues regarding the new test format here.

Why?

Just a brief (though opinionated) list of things unlocked by the new API:

  • Users can clearly understand which parts of their tests have to do with QUnit and which have to do with ember-qunit.
  • Migrates things like this.subject(), this.register(...), this.inject.service(...) away from one-off concepts (which only existed in unit and integration tests) towards concepts that apply universally.
  • It is trivial for an addon to easily provide its own custom test setup without reinventing the world. For example, ember-data may now provide its own setupSerializerTest or setupAdapterTest helper methods to significantly ease the boilerplate for testing those object types. This might look like:
// ...snip imports...
import { setupSerializerTest } from 'ember-data/test-support';

module('post serializer', function(hooks) {
  setupTest(hooks);
  setupSerializerTest(hooks, 'post');
  
  test('can do stuff here now', function(assert) {
    // then the test could rely on some helper methods 
    // made available by ember-data's test helpers...
    let actual = this.serialize(/* some payload here */);
    assert.deepEqual(actual, {
      /* whatever you expect */
    });
  });
});
  • Nested test modules (which have been available for quite some time in QUnit) are now available when testing in Ember. This means you can do:
// ...snip imports...

module('x-foo', function(hooks) {
  setupRenderingTest(hooks);
  
  module('for admin users', function(hooks) {
    // userland helper method for setting up tests with auth
    setupAuthenticatedUser(hooks, 'admin');
    
    test('displays one way', async function(assert) {
      assert.expect(1);

      await render(hbs`{{x-foo name="red"}}`);

      assert.equal(this.element.textContent, 'something here');
    });
  });
  
  module('for non-admin users', function(hooks) {
    // userland helper method for setting up tests with auth
    setupAuthenticatedUser(hooks, 'user');
    
    test('displays a different way', async function(assert) {
      assert.expect(1);

      await render(hbs`{{x-foo name="red"}}`);

      assert.equal(this.element.textContent, 'something else here');
    });
  });
});

Design๐Ÿ•ต๏ธ

The new API was originally pioneered by Tobias Bieniek (in emberjs/ember-mocha#84)๐Ÿ‘๐Ÿ‘๐Ÿ‘, subsequently tweaked with an eye towards "Grand Testing Unification" and QUnit compatibility before being submitted as emberjs/rfcs#232. The primary goal of the change is to remove the coupling between ember-qunit and QUnit itself, so that folks can begin leveraging the new features that the QUnit team has been working on.

A quick summary of things from the RFC are just below (if you are familiar with the RFC go ahead and skip to the next section).

Details

QUnit "nested modules"

The new API leverages QUnit's "nested module" syntax which has been available as part of QUnit 1.20.0 (all the way back in 2015!). Since we haven't been able to use the nested syntax in Ember before, here's how it works.

With nested modules, a normal QUnit 1.x module setup changes from:

QUnit.module('some description', {
  before() {},
  beforeEach() {},
  afterEach() {},
  after() {}
});

QUnit.test('it blends', function(assert) {
  assert.ok(true, 'of course!');
});

Into:

QUnit.module('some description', function(hooks) {

  hooks.before(function() { });
  hooks.beforeEach(function() { });
  hooks.afterEach(function() { });
  hooks.after(function() { });

  QUnit.test('it blends', function(assert) {
    assert.ok(true, 'of course!');
  });
});

This makes it much simpler to support multiple before, beforeEach, afterEach, and after callbacks, and it also allows for arbitrary nesting of modules.

You can read more about QUnit nested modules in the QUnit documentation.

setupTest

The setupTest method is used for all types of tests except for those that need to render snippets of templates. It is invoked in the callback scope of a QUnit module (aka "nested module"):

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('foo service', function(hooks) {
  setupTest(hooks);
  
  // ...snip...
});

Once invoked, all subsequent hooks.beforeEach and test invocations will have access to the following:

  • this.owner - This exposes the standard "owner API" for the test environment.
  • this.set / this.setProperties - Allows setting values on the test context.
  • this.get / this.getProperties - Retrieves values from the test context.

setupRenderingTest

The setupRenderingTest method is used for tests that need to render snippets of templates. It is also invoked in the callback scope of a QUnit module (aka "nested module"):

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

module('foo service', function(hooks) {
  setupRenderingTest(hooks);
  
  // ...snip...
});

Once invoked, all subsequent hooks.beforeEach and test invocations will have access to the following:

  • All of the methods / properties listed for setupTest
  • this.render(...) - Renders the provided template snippet returning a promise that resolves once rendering has completed
  • An importable render function that de-sugars into this.render will be the default output of blueprints
  • this.element - Returns the native DOM element representing the element that was rendered via this.render
  • this.$(...) - When jQuery is present, executes a jQuery selector with the current this.element as its root

Significant Changes

Here is a list of the more significant changes (stolen from the RFC ๐Ÿ˜ˆ):

  • the various setup methods no longer need to know the name of the object under test
  • this.subject is removed in favor of using the Ember owner API for looking up and creating instances (this.owner.lookup and this.owner.factoryFor)
  • this.inject is removed in favor of using this.owner.lookup directly
  • this.register is removed in favor of using this.owner.register directly
  • this.render will now return a promise that will fulfill when rendering is complete (this allows for further iteration in the rendering engine to avoid blocking the main thread)
  • An importable render helper will be used by default (instead of this.render) to help reduce "rightward shift" when used with async/await
  • this.element is being introduced as a public API for DOM assertions in a jQuery-less environment
  • QUnit nested modules are required

Rewrite my test suiteโ‰๏ธโ‰๏ธ๐Ÿ˜ฑ

Migrating an applications test suite to the new APIs may be a time consuming and laborious process. In order to help folks through this transition the team has a few goodies in store...

Existing APIs

All of the changes are additive which means that the existing API (moduleFor*, et al) will remain available for quite some time. You will be able to use both moduleFor style APIs and the newer setupTest APIs within the same test suite.

The release of ember-qunit@3 will not include deprecations of the moduleFor APIs. They will only be deprecated once enough users have been able to update and we become more confident in the stability of the new API.

Codemod ๐ŸŽ

We all hate doing work that could be done better and faster by our computers. So we have created ember-qunit-codemod which handles nearly all of the work when migrating. For example, a number of folks have been able to run the codemod and things "just work", others have had a vast majority of their tests pass by running the codemod and only have to manually fix a handful of failures.

Running the codemod is as simple as:

npm install --global jscodeshift
jscodeshift -t https://rawgit.com/rwjblue/ember-qunit-codemod/master/ember-qunit-codemod.js ./tests

As with many codemods, this codemod is not quite perfect. The main difference in semantics is that memoization (of this.subject() or any of the methods passed in to moduleFor in options) has been removed.

Please report any issues you run into with the codemod here.

How do I use it? ๐ŸŽฉ

The following should be all that you need to get up and running with the new APIs:

# if used, update to the latest version of linting addons
# to fix a bug in detecting beta versions of ember-cli-qunit
npm install --save-dev ember-cli-eslint
npm install --save-dev ember-cli-template-lint

# update to the beta version of ember-cli-qunit / ember-qunit
npm install --save-dev ember-cli-qunit@^4.1.0-beta.2

# migrate your current tests
npm install --global jscodeshift
jscodeshift -t https://rawgit.com/rwjblue/ember-qunit-codemod/master/ember-qunit-codemod.js ./tests

# run your tests
ember test

Report issues running the codemod here and issues with running the tests here.

Where can I get more infoโ“

For the complete details on the API changes, please review emberjs/rfcs#232 and watch for the ember-qunit README to be updated as we move the new API out of beta to become the primary testing API.