Ember Testing Internals
with Ember-CLI
Cory Forsyth
@bantic
201 Created
Matthew BealeCory Forsyth
http://201-created.com/
http://devopsreactions.tumblr.com/
The Ember-CLI Testing Triumvirate
• The test harness (tests/index.html)
• Unit Test Affordances
• Acceptance Test Affordances
$ ember new my-app
Ember-CLI makes testing
Easy
• `ember generate X` creates test for X
• 14 test types:
• acceptance, adapter, component, controller,
• helper, initializer, mixin, model, route,
• serializer, service, transform, util, view
Ember-CLI Test Harness
• A real strength of Ember-CLI
• Ember-CLI builds tests/index.html for you
• QUnit is built-in (more on this later)
<!DOCTYPE html>!
<html>!
<head>!
<meta charset="utf-8">!
<meta http-equiv="X-UA-Compatible" content="IE=edge">!
<title>EmberTestingTalk Tests</title>!
<meta name="description" content="">!
<meta name="viewport" content="width=device-width, initial-scale=1">!
!
{{content-for 'head'}}!
{{content-for 'test-head'}}!
!
<link rel="stylesheet" href="assets/vendor.css">!
<link rel="stylesheet" href="assets/ember-testing-talk.css">!
<link rel="stylesheet" href="assets/test-support.css">!
<style>!
#ember-testing-container {!
position: absolute;!
background: white;!
bottom: 0;!
right: 0;!
width: 640px;!
height: 384px;!
overflow: auto;!
z-index: 9999;!
border: 1px solid #ccc;!
}!
#ember-testing {!
zoom: 50%;!
}!
</style>!
</head>!
config in meta tag
addons can modify
Ember-CLI builds these
makes that mini-me
app on the test page
tests/index.html
<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>!
for QUnit
addons can modify
tests/index.html
<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
/* 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
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
$ 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);!
});!
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
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)
Ember-CLI Test Harness
Anatomy of a Unit Test
• How does Ember actually run a unit test?
• What does that boilerplate do?
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
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
ember-qunit
• imported via ember-cli-qunit addon
• provides `moduleFor`
• also: `moduleForModel`, `moduleForComponent`
• provides `test`
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
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
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
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!
ember-cli-mocha
Ember Testing Affordances
• Two primary types of tests in Ember:
• Unit Tests
• need isolated containers, specific setup
• use moduleFor
Ember Testing Affordances
• Two primary types of tests in Ember:
• Unit Tests and
• Acceptance Tests
• Totally different animal
• must manage async, interact with DOM
Ember Acceptance Tests
$ ember g acceptance-test index
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');!
});!
});!
tests/unit/controllers/index-test.js
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');!
});!
});!
tests/unit/controllers/index-test.js
What if visiting / takes
5 seconds?
How does this know to wait?
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 if visiting / takes
5 seconds?
How does this know to wait?
tests/unit/controllers/index-test.js
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
tests/acceptance/index-test.js
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
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
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
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
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
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
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
Timeline
Test.lastPromise
Code
visit(‘/posts’); fillIn(‘input’); click(‘.submit’);
.then .then .then
visit(‘/posts’);
fillIn(‘input’);
click(‘.submit’);
magic ember async chaining
Ember Sync Test Helpers
• Used for inspecting app state or DOM
• find(selector) — just like jQuery(selector)
• currentPathName()
• currentRouteName()
• currentURL()
• pauseTest() — new!
Ember Async Test Helpers
• visit(url)
• fillIn(selector, text)
• click(selector)
• keyEvent(selector, keyCode)
• andThen(callback)
• wait() — this one is special
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()`
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()
A good test & framework
should guide you
visit(‘/foo’) The URL '/foo' did not match any routes …
click(‘input.button’) Element input.button not found.
Error messages can guide you, sometimes
? TypeError: Cannot read property 'get' of undefined
but not all the time
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
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
• (alpha)
• `npm install —save-dev ember-cli-acceptance-test-helpers`
• expectComponent(componentName)
• clickComponent(componentName)
• expectElement(selector)
• withinElement(), expectInput() — coming soon
ember-cli-acceptance-test-helpers
• 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
http://devopsreactions.tumblr.com/
testing your own code
doesn’t have to be like this
Thank you
Cory Forsyth
@bantic
Photo credits
! !
http://devopsreactions.tumblr.com/!
www.ohmagif.com
Cory Forsyth
@bantic
Photo credits
! !
http://devopsreactions.tumblr.com/!
www.ohmagif.com
• Slides: http://bit.ly/ember-testing-talk-to
• ember-test-helpers
• ember-cli-acceptance-test-helpers
• ember-cli-mocha
• setupForTesting()
• injectTestHelpers()
• wait() async test helper
• ember-cli-qunit
• ember-qunit
Links

Ember testing internals with ember cli

  • 1.
    Ember Testing Internals withEmber-CLI Cory Forsyth @bantic
  • 2.
    201 Created Matthew BealeCoryForsyth http://201-created.com/
  • 3.
  • 4.
    The Ember-CLI TestingTriumvirate • The test harness (tests/index.html) • Unit Test Affordances • Acceptance Test Affordances
  • 5.
  • 7.
    Ember-CLI makes testing Easy •`ember generate X` creates test for X • 14 test types: • acceptance, adapter, component, controller, • helper, initializer, mixin, model, route, • serializer, service, transform, util, view
  • 8.
    Ember-CLI Test Harness •A real strength of Ember-CLI • Ember-CLI builds tests/index.html for you • QUnit is built-in (more on this later)
  • 9.
    <!DOCTYPE html>! <html>! <head>! <meta charset="utf-8">! <metahttp-equiv="X-UA-Compatible" content="IE=edge">! <title>EmberTestingTalk Tests</title>! <meta name="description" content="">! <meta name="viewport" content="width=device-width, initial-scale=1">! ! {{content-for 'head'}}! {{content-for 'test-head'}}! ! <link rel="stylesheet" href="assets/vendor.css">! <link rel="stylesheet" href="assets/ember-testing-talk.css">! <link rel="stylesheet" href="assets/test-support.css">! <style>! #ember-testing-container {! position: absolute;! background: white;! bottom: 0;! right: 0;! width: 640px;! height: 384px;! overflow: auto;! z-index: 9999;! border: 1px solid #ccc;! }! #ember-testing {! zoom: 50%;! }! </style>! </head>! config in meta tag addons can modify Ember-CLI builds these makes that mini-me app on the test page tests/index.html
  • 10.
    <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>! for QUnit addons can modify tests/index.html
  • 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("thistest 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 gcontroller 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)
  • 17.
  • 18.
    Anatomy of aUnit 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
  • 21.
    ember-qunit • imported viaember-cli-qunit addon • provides `moduleFor` • also: `moduleForModel`, `moduleForComponent` • provides `test`
  • 22.
    ember-qunit: moduleFor • wrapsQUnit’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 • wrapsQUnit’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 onember-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!
  • 26.
  • 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
  • 29.
    Ember Acceptance Tests $ember g acceptance-test index
  • 30.
    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');! });! });! tests/unit/controllers/index-test.js
  • 31.
    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');! });! });! tests/unit/controllers/index-test.js What if visiting / takes 5 seconds? How does this know to wait?
  • 32.
    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 if visiting / takes 5 seconds? How does this know to wait? tests/unit/controllers/index-test.js
  • 33.
    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 tests/acceptance/index-test.js
  • 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 upall 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
  • 41.
    Timeline Test.lastPromise Code visit(‘/posts’); fillIn(‘input’); click(‘.submit’); .then.then .then visit(‘/posts’); fillIn(‘input’); click(‘.submit’); magic ember async chaining
  • 42.
    Ember Sync TestHelpers • Used for inspecting app state or DOM • find(selector) — just like jQuery(selector) • currentPathName() • currentRouteName() • currentURL() • pauseTest() — new!
  • 43.
    Ember Async TestHelpers • 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()
  • 46.
    A good test& framework should guide you
  • 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
  • 48.
    ? TypeError: Cannotread property 'get' of undefined but not all the time
  • 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
  • 51.
    • (alpha) • `npminstall —save-dev ember-cli-acceptance-test-helpers` • expectComponent(componentName) • clickComponent(componentName) • expectElement(selector) • withinElement(), expectInput() — coming soon ember-cli-acceptance-test-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
  • 53.
    http://devopsreactions.tumblr.com/ testing your owncode doesn’t have to be like this
  • 54.
    Thank you Cory Forsyth @bantic Photocredits ! ! http://devopsreactions.tumblr.com/! www.ohmagif.com
  • 55.
    Cory Forsyth @bantic Photo credits !! http://devopsreactions.tumblr.com/! www.ohmagif.com • Slides: http://bit.ly/ember-testing-talk-to • ember-test-helpers • ember-cli-acceptance-test-helpers • ember-cli-mocha • setupForTesting() • injectTestHelpers() • wait() async test helper • ember-cli-qunit • ember-qunit Links