๐ 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 ownsetupSerializerTest
orsetupAdapterTest
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 intothis.render
will be the default output of blueprints this.element
- Returns the native DOM element representing the element that was rendered viathis.render
this.$(...)
- When jQuery is present, executes a jQuery selector with the currentthis.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
andthis.owner.factoryFor
)this.inject
is removed in favor of usingthis.owner.lookup
directlythis.register
is removed in favor of usingthis.owner.register
directlythis.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 ofthis.render
) to help reduce "rightward shift" when used withasync
/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.