11. <body>!
<div id="qunit"></div>!
<div id="qunit-fixture"></div>!
!
{{content-for 'body'}}!
{{content-for 'test-body'}}!
<script src="assets/vendor.js"></script>!
<script src="assets/test-support.js"></script>!
<script src="assets/ember-testing-talk.js"></script>!
<script src="testem.js"></script>!
<script src="assets/test-loader.js"></script>!
</body>!
</html>!
jQuery, Handlebars,
Ember, `app.import`
QUnit, ember-qunit
app code, including
tests (in non-prod env)
app code, including
tests (in non-prod env)
`require`s all the tests
tests/index.html
12. /* globals requirejs, require */!
!
var moduleName, shouldLoad;!
!
QUnit.config.urlConfig.push({ id: 'nojshint', label: 'Disable JSHint'});!
!
// TODO: load based on params!
for (moduleName in requirejs.entries) {!
shouldLoad = false;!
!
if (moduleName.match(/[-_]test$/)) { shouldLoad = true; }!
if (!QUnit.urlParams.nojshint && moduleName.match(/.jshint$/)) { shouldLoad = true; }!
!
if (shouldLoad) { require(moduleName); }!
}!
!
if (QUnit.notifications) {!
QUnit.notifications({!
icons: {!
passed: '/assets/passed.png',!
failed: '/assets/failed.png'!
}!
});!
}!
Requires every module name ending in _test or -test
(named AMD modules, not npm modules or QUnit modules)
test-loader.js
13. module("a basic test");!
!
test("this test will pass", function(){!
ok(true, "yep, it did");!
});!
define("ember-testing-talk/tests/unit/basic-test", [], function(){!
! "use strict";!
! module("a basic test");!
!
! test("this test will pass", function(){!
! ! ok(true, "yep, it did");!
! });!
});
test-loader.js requires this, QUnit runs it
Ember-CLI compiles to
named AMD module ending in -test
tests/unit/basic-test.js
14. $ ember g controller index
import {!
moduleFor,!
test!
} from 'ember-qunit';!
!
moduleFor('controller:index', 'IndexController', {!
// Specify the other units that are required for this test.!
// needs: ['controller:foo']!
});!
!
// Replace this with your real tests.!
test('it exists', function() {!
var controller = this.subject();!
ok(controller);!
});!
15. Ember-CLI Test Harness
• tests/index.html:
• app code as named AMD modules
• app test code as named AMD modules
• vendor js (Ember, Handlebars, jQuery)
• test support (QUnit, ember-qunit AMD)
• test-loader.js: `require`s each AMD test module
• QUnit runs the tests
16. Ember-CLI Test Harness
• How does QUnit and ember-qunit end up in test-
support.js?
• ember-cli-qunit! (it is an ember-cli addon)
18. Anatomy of a Unit Test
• How does Ember actually run a unit test?
• What does that boilerplate do?
19. import {!
moduleFor,!
test!
} from 'ember-qunit';!
!
moduleFor('controller:index', 'IndexController', {!
// Specify the other units that are required for this test.!
// needs: ['controller:foo']!
});!
!
// Replace this with your real tests.!
test('it exists', function() {!
var controller = this.subject();!
ok(controller);!
});!
tests/unit/controllers/index-test.js
20. import {!
moduleFor,!
test!
} from 'ember-qunit';!
!
moduleFor('controller:index', 'IndexController', {!
// Specify the other units that are required for this test.!
// needs: ['controller:foo']!
});!
!
// Replace this with your real tests.!
test('it exists', function() {!
var controller = this.subject();!
ok(controller);!
});!
tests/unit/controllers/index-test.js
22. ember-qunit: moduleFor
• wraps QUnit’s native `QUnit.module`
• creates an isolated container with `needs` array
• provides a context for test:
• this.subject(), this.container, etc
23. ember-qunit: moduleForX
• moduleForComponent
• registers my-component.js and my-component.hbs
• connects the template to the component as ‘layout’
• adds `this.render`, `this.append` and `this.$`
• moduleForModel
• sets up ember-data (registers default transforms, etc)
• adds `this.store()`
• registers application:adapter, defaults to DS.FixtureAdapter
24. ember-qunit: test
• wraps QUnit’s native `QUnit.test`
• casts the test function result to a promise
• uses `stop` and `start` to handle potential async
• if you `return` a promise, the test will handle it
correctly
• runs the promise resolution in an Ember.run loop
25. ember-qunit
• Builds on ember-test-helpers (library)
• ember-test-helpers is test-framework-agnostic
• provides methods for creating test suites (aka
QUnit modules), setup/teardown, etc
• future framework adapters can build on it
• ember-cli-mocha!
27. Ember Testing Affordances
• Two primary types of tests in Ember:
• Unit Tests
• need isolated containers, specific setup
• use moduleFor
28. Ember Testing Affordances
• Two primary types of tests in Ember:
• Unit Tests and
• Acceptance Tests
• Totally different animal
• must manage async, interact with DOM
34. import Ember from 'ember';!
import startApp from '../helpers/start-app';!
!
var App;!
!
module('Acceptance: Index', {!
setup: function() {!
App = startApp();!
},!
teardown: function() {!
Ember.run(App, 'destroy');!
}!
});!
!
test('visiting /', function() {!
visit('/');!
!
andThen(function() {!
equal(currentPath(), 'index');!
});!
});!
vanilla QUnit module
special test helpers:
visit, andThen,
currentPath
tests/acceptance/index-test.js
35. import Ember from 'ember';!
import startApp from '../helpers/start-app';!
!
var App;!
!
module('Acceptance: Index', {!
setup: function() {!
App = startApp();!
},!
teardown: function() {!
Ember.run(App, 'destroy');!
}!
});!
!
test('visiting /', function() {!
visit('/');!
!
andThen(function() {!
equal(currentPath(), 'index');!
});!
});!
What is `startApp`?
tests/acceptance/index-test.js
36. import Ember from 'ember';!
import Application from '../../app';!
import Router from '../../router';!
import config from '../../config/environment';!
!
export default function startApp(attrs) {!
var App;!
!
var attributes = Ember.merge({}, config.APP);!
attributes = Ember.merge(attributes, attrs);!
!
Router.reopen({!
location: 'none'!
});!
!
Ember.run(function() {!
App = Application.create(attributes);!
App.setupForTesting();!
App.injectTestHelpers();!
});!
!
App.reset();!
!
return App;!
}!
don’t change URL
start application
tests/helpers/start_app.js
37. import Ember from 'ember';!
import Application from '../../app';!
import Router from '../../router';!
import config from '../../config/environment';!
!
export default function startApp(attrs) {!
var App;!
!
var attributes = Ember.merge({}, config.APP);!
attributes = Ember.merge(attributes, attrs);!
!
Router.reopen({!
location: 'none'!
});!
!
Ember.run(function() {!
App = Application.create(attributes);!
App.setupForTesting();!
App.injectTestHelpers();!
});!
!
App.reset();!
!
return App;!
}!
• set Ember.testing = true
• set a test adapter
• prep for ajax:
• listeners for ajaxSend,
ajaxComplete
tests/helpers/start_app.js
38. import Ember from 'ember';!
import Application from '../../app';!
import Router from '../../router';!
import config from '../../config/environment';!
!
export default function startApp(attrs) {!
var App;!
!
var attributes = Ember.merge({}, config.APP);!
attributes = Ember.merge(attributes, attrs);!
!
Router.reopen({!
location: 'none'!
});!
!
Ember.run(function() {!
App = Application.create(attributes);!
App.setupForTesting();!
App.injectTestHelpers();!
});!
!
App.reset();!
!
return App;!
}!
• wrap all registered test helpers
• 2 types: sync and async
tests/helpers/start_app.js
39. injectTestHelpers
• sets up all existing registered test helpers,
including built-ins (find, visit, click, etc) on `window`
• each helper fn closes over the running app
• sync helper: returns value of running the helper
• async helper: complicated code to detect when
async behavior (routing, promises, ajax) is in
progress
40. function helper(app, name) {!
var fn = helpers[name].method;!
var meta = helpers[name].meta;!
!
return function() {!
var args = slice.call(arguments);!
var lastPromise = Test.lastPromise;!
!
args.unshift(app);!
!
// not async!
if (!meta.wait) {!
return fn.apply(app, args);!
}!
!
if (!lastPromise) {!
// It's the first async helper in current context!
lastPromise = fn.apply(app, args);!
} else {!
// wait for last helper's promise to resolve!
// and then execute!
run(function() {!
lastPromise = Test.resolve(lastPromise).then(function() {!
return fn.apply(app, args);!
});!
});!
}!
!
return lastPromise;!
};!
}!
Test.lastPromise “global”
chain onto the existing test
promise!
inside injectTestHelpers
42. Ember Sync Test Helpers
• Used for inspecting app state or DOM
• find(selector) — just like jQuery(selector)
• currentPathName()
• currentRouteName()
• currentURL()
• pauseTest() — new!
43. Ember Async Test Helpers
• visit(url)
• fillIn(selector, text)
• click(selector)
• keyEvent(selector, keyCode)
• andThen(callback)
• wait() — this one is special
44. How does `wait` know to
wait?
• polling!
• check for active router transition
• check for pending ajax requests
• check if active runloop or Ember.run.later scheduled
• check for user-specified async via
registerWaiter(callback)
• all async helpers must return a call to `wait()`
45. function wait(app, value) {!
return Test.promise(function(resolve) {!
// If this is the first async promise, kick off the async test!
if (++countAsync === 1) {!
Test.adapter.asyncStart();!
}!
!
// Every 10ms, poll for the async thing to have finished!
var watcher = setInterval(function() {!
// 1. If the router is loading, keep polling!
var routerIsLoading = !!app.__container__.lookup('router:main').router.activeTransition;!
if (routerIsLoading) { return; }!
!
// 2. If there are pending Ajax requests, keep polling!
if (Test.pendingAjaxRequests) { return; }!
!
// 3. If there are scheduled timers or we are inside of a run loop, keep polling!
if (run.hasScheduledTimers() || run.currentRunLoop) { return; }!
if (Test.waiters && Test.waiters.any(function(waiter) {!
var context = waiter[0];!
var callback = waiter[1];!
return !callback.call(context);!
})) { return; }!
// Stop polling!
clearInterval(watcher);!
!
// If this is the last async promise, end the async test!
if (--countAsync === 0) {!
Test.adapter.asyncEnd();!
}!
!
// Synchronously resolve the promise!
run(null, resolve, value);!
}, 10);!
});!
}!
check for ajax
poll every 10ms
check for active routing
transition
check user-registered
waiters via registerWaiter()
wait()
47. visit(‘/foo’) The URL '/foo' did not match any routes …
click(‘input.button’) Element input.button not found.
Error messages can guide you, sometimes
49. Ember.Test.registerAsyncHelper('signIn', function(app) {!
! visit('/signin');!
! fillIn('input.email', 'abc@def.com');!
! fillIn('input.password', 'secret');!
! click('button.sign-in');!
});!
test('signs in and then does X', function(){!
signIn();!
!
andThen(function(){!
!// ... I am signed in!!
});!
});!
Use domain-specific async helpers
50. Ember.Test.registerHelper('navbarContains', function(app, text)
{!
! var el = find('.nav-bar:contains(' + text + ')');!
! ok(el.length, 'has a nav bar with text: ' + text);!
});!
test('sees name in nav-bar', function(){!
! visit('/');!
! andThen(function(){!
! ! navbarContains('My App');!
! });!
});!
Use domain-specific sync helpers
52. • expectComponent
• clickComponent!
!
• expectElement
No component called X was found in the container
Expected to find component X
Found 3 of .some-div but expected 2
Found 1 of .some-div but 0 containing “some text”
ember-cli-acceptance-test-helpers