SlideShare a Scribd company logo
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 testing internals with ember cli
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

More Related Content

What's hot (20)

PDF
AngularJS Unit Test
Chiew Carol
 
PDF
Unit Testing Express and Koa Middleware in ES2015
Morris Singer
 
PDF
Test-Driven Development of AngularJS Applications
FITC
 
PDF
Intro to testing Javascript with jasmine
Timothy Oxley
 
PDF
You do not need automation engineer - Sqa Days - 2015 - EN
Iakiv Kramarenko
 
PDF
AngularJS Unit Testing w/Karma and Jasmine
foxp2code
 
PPTX
Full Stack Unit Testing
GlobalLogic Ukraine
 
PDF
Unit Testing JavaScript Applications
Ynon Perek
 
PPTX
AngularJS Unit Testing
Prince Norin
 
PDF
Angularjs - Unit testing introduction
Nir Kaufman
 
PPT
Testing in AngularJS
Peter Drinnan
 
PDF
Painless JavaScript Testing with Jest
Michał Pierzchała
 
PDF
Unit Testing Express Middleware
Morris Singer
 
PDF
Ember and containers
Matthew Beale
 
PPTX
Rapid prototyping and easy testing with ember cli mirage
Krzysztof Bialek
 
PDF
Jasmine BDD for Javascript
Luis Alfredo Porras Páez
 
PPTX
Unit testing in JavaScript with Jasmine and Karma
Andrey Kolodnitsky
 
ODP
Angular JS Unit Testing - Overview
Thirumal Sakthivel
 
PDF
Painless Javascript Unit Testing
Benjamin Wilson
 
PDF
Angular testing
Raissa Ferreira
 
AngularJS Unit Test
Chiew Carol
 
Unit Testing Express and Koa Middleware in ES2015
Morris Singer
 
Test-Driven Development of AngularJS Applications
FITC
 
Intro to testing Javascript with jasmine
Timothy Oxley
 
You do not need automation engineer - Sqa Days - 2015 - EN
Iakiv Kramarenko
 
AngularJS Unit Testing w/Karma and Jasmine
foxp2code
 
Full Stack Unit Testing
GlobalLogic Ukraine
 
Unit Testing JavaScript Applications
Ynon Perek
 
AngularJS Unit Testing
Prince Norin
 
Angularjs - Unit testing introduction
Nir Kaufman
 
Testing in AngularJS
Peter Drinnan
 
Painless JavaScript Testing with Jest
Michał Pierzchała
 
Unit Testing Express Middleware
Morris Singer
 
Ember and containers
Matthew Beale
 
Rapid prototyping and easy testing with ember cli mirage
Krzysztof Bialek
 
Jasmine BDD for Javascript
Luis Alfredo Porras Páez
 
Unit testing in JavaScript with Jasmine and Karma
Andrey Kolodnitsky
 
Angular JS Unit Testing - Overview
Thirumal Sakthivel
 
Painless Javascript Unit Testing
Benjamin Wilson
 
Angular testing
Raissa Ferreira
 

Viewers also liked (15)

PDF
Stackup New Languages Talk: Ember is for Everybody
Cory Forsyth
 
KEY
APIs: Internet for Robots
Cory Forsyth
 
PDF
EmberFest Mobiledoc Demo Lightning Talk
Cory Forsyth
 
PDF
Making ember-wormhole work with Fastboot
Cory Forsyth
 
PDF
Microsoft tech talk march 28 2014
Cory Forsyth
 
PDF
OAuth 2.0 Misconceptions
Cory Forsyth
 
PDF
Chrome Extensions at Manhattan JS
Cory Forsyth
 
PDF
E tutorial -
PSPCL
 
PDF
Torii: Ember.js Authentication Library
Cory Forsyth
 
PDF
Gradle起步走: 以CLI Application為例 @ JCConf 2014
Chen-en Lu
 
PDF
HTTP by Hand: Exploring HTTP/1.0, 1.1 and 2.0
Cory Forsyth
 
PPTX
Introduction to HTTP/2
Ido Flatow
 
PDF
Ericsson License Assisted Access (LAA) January 2015
Ericsson
 
PDF
Lte epc trial experience
Hussien Mahmoud
 
PDF
What HTTP/2.0 Will Do For You
Mark Nottingham
 
Stackup New Languages Talk: Ember is for Everybody
Cory Forsyth
 
APIs: Internet for Robots
Cory Forsyth
 
EmberFest Mobiledoc Demo Lightning Talk
Cory Forsyth
 
Making ember-wormhole work with Fastboot
Cory Forsyth
 
Microsoft tech talk march 28 2014
Cory Forsyth
 
OAuth 2.0 Misconceptions
Cory Forsyth
 
Chrome Extensions at Manhattan JS
Cory Forsyth
 
E tutorial -
PSPCL
 
Torii: Ember.js Authentication Library
Cory Forsyth
 
Gradle起步走: 以CLI Application為例 @ JCConf 2014
Chen-en Lu
 
HTTP by Hand: Exploring HTTP/1.0, 1.1 and 2.0
Cory Forsyth
 
Introduction to HTTP/2
Ido Flatow
 
Ericsson License Assisted Access (LAA) January 2015
Ericsson
 
Lte epc trial experience
Hussien Mahmoud
 
What HTTP/2.0 Will Do For You
Mark Nottingham
 
Ad

Similar to Ember testing internals with ember cli (20)

PDF
Ember.js Brussels Meetup #3 - Testing your Ember.js app
yoranbe
 
PDF
(Unit) Testing in Emberjs – Munich Ember.js Meetup July 2014
istefo
 
PPT
Ember.js: Jump Start
Viacheslav Bukach
 
PDF
Journey to the Center of Ember Test Helpers
Julien Fitzpatrick
 
PDF
Maintaining Your Tests At Scale
Trent Willis
 
PDF
Workshop 16: EmberJS Parte I
Visual Engineering
 
PPTX
Ember - introduction
Harikrishnan C
 
PDF
The road to Ember 2.0
Filippo Zanella
 
PDF
Ember presentation
Daniel N
 
PPTX
Intro to EmberJS
Billy Onjea
 
PDF
Viliam Elischer - Ember.js - Jak zatopit a neshořet!
Develcz
 
PDF
Intro to ember.js
Leo Hernandez
 
PDF
Ember CLI & Ember Tooling
Mark Provan
 
PDF
Introduction to Ember.js and how we used it at FlowPro.io
Paul Knittel
 
PDF
Ember.js - Jak zatopit a neshořet!
Viliam Elischer
 
PPTX
The road to Ember.js 2.0
Codemotion
 
PPTX
Full slidescr16
Lucio Grenzi
 
PPTX
Node.js meetup 17.05.2017 ember.js - escape the javascript fatigue
Tobias Braner
 
PDF
Delivering with ember.js
Andrei Sebastian Cîmpean
 
PDF
Ember.js - Harnessing Convention Over Configuration
Tracy Lee
 
Ember.js Brussels Meetup #3 - Testing your Ember.js app
yoranbe
 
(Unit) Testing in Emberjs – Munich Ember.js Meetup July 2014
istefo
 
Ember.js: Jump Start
Viacheslav Bukach
 
Journey to the Center of Ember Test Helpers
Julien Fitzpatrick
 
Maintaining Your Tests At Scale
Trent Willis
 
Workshop 16: EmberJS Parte I
Visual Engineering
 
Ember - introduction
Harikrishnan C
 
The road to Ember 2.0
Filippo Zanella
 
Ember presentation
Daniel N
 
Intro to EmberJS
Billy Onjea
 
Viliam Elischer - Ember.js - Jak zatopit a neshořet!
Develcz
 
Intro to ember.js
Leo Hernandez
 
Ember CLI & Ember Tooling
Mark Provan
 
Introduction to Ember.js and how we used it at FlowPro.io
Paul Knittel
 
Ember.js - Jak zatopit a neshořet!
Viliam Elischer
 
The road to Ember.js 2.0
Codemotion
 
Full slidescr16
Lucio Grenzi
 
Node.js meetup 17.05.2017 ember.js - escape the javascript fatigue
Tobias Braner
 
Delivering with ember.js
Andrei Sebastian Cîmpean
 
Ember.js - Harnessing Convention Over Configuration
Tracy Lee
 
Ad

Recently uploaded (20)

PPTX
The Role of Information Technology in Environmental Protectio....pptx
nallamillisriram
 
PPTX
Thermal runway and thermal stability.pptx
godow93766
 
PPTX
GitOps_Without_K8s_Training_detailed git repository
DanialHabibi2
 
PPTX
Shinkawa Proposal to meet Vibration API670.pptx
AchmadBashori2
 
PDF
MAD Unit - 1 Introduction of Android IT Department
JappanMavani
 
DOCX
CS-802 (A) BDH Lab manual IPS Academy Indore
thegodhimself05
 
PDF
Water Industry Process Automation & Control Monthly July 2025
Water Industry Process Automation & Control
 
PPTX
Presentation 2.pptx AI-powered home security systems Secure-by-design IoT fr...
SoundaryaBC2
 
PPTX
2025 CGI Congres - Surviving agile v05.pptx
Derk-Jan de Grood
 
PPTX
Evaluation and thermal analysis of shell and tube heat exchanger as per requi...
shahveer210504
 
PPTX
fatigue in aircraft structures-221113192308-0ad6dc8c.pptx
aviatecofficial
 
PDF
Basic_Concepts_in_Clinical_Biochemistry_2018كيمياء_عملي.pdf
AdelLoin
 
PDF
MAD Unit - 2 Activity and Fragment Management in Android (Diploma IT)
JappanMavani
 
PPTX
Arduino Based Gas Leakage Detector Project
CircuitDigest
 
PPTX
Worm gear strength and wear calculation as per standard VB Bhandari Databook.
shahveer210504
 
PDF
Zilliz Cloud Demo for performance and scale
Zilliz
 
PPTX
What is Shot Peening | Shot Peening is a Surface Treatment Process
Vibra Finish
 
PDF
Biomechanics of Gait: Engineering Solutions for Rehabilitation (www.kiu.ac.ug)
publication11
 
PDF
Introduction to Productivity and Quality
মোঃ ফুরকান উদ্দিন জুয়েল
 
PDF
Pressure Measurement training for engineers and Technicians
AIESOLUTIONS
 
The Role of Information Technology in Environmental Protectio....pptx
nallamillisriram
 
Thermal runway and thermal stability.pptx
godow93766
 
GitOps_Without_K8s_Training_detailed git repository
DanialHabibi2
 
Shinkawa Proposal to meet Vibration API670.pptx
AchmadBashori2
 
MAD Unit - 1 Introduction of Android IT Department
JappanMavani
 
CS-802 (A) BDH Lab manual IPS Academy Indore
thegodhimself05
 
Water Industry Process Automation & Control Monthly July 2025
Water Industry Process Automation & Control
 
Presentation 2.pptx AI-powered home security systems Secure-by-design IoT fr...
SoundaryaBC2
 
2025 CGI Congres - Surviving agile v05.pptx
Derk-Jan de Grood
 
Evaluation and thermal analysis of shell and tube heat exchanger as per requi...
shahveer210504
 
fatigue in aircraft structures-221113192308-0ad6dc8c.pptx
aviatecofficial
 
Basic_Concepts_in_Clinical_Biochemistry_2018كيمياء_عملي.pdf
AdelLoin
 
MAD Unit - 2 Activity and Fragment Management in Android (Diploma IT)
JappanMavani
 
Arduino Based Gas Leakage Detector Project
CircuitDigest
 
Worm gear strength and wear calculation as per standard VB Bhandari Databook.
shahveer210504
 
Zilliz Cloud Demo for performance and scale
Zilliz
 
What is Shot Peening | Shot Peening is a Surface Treatment Process
Vibra Finish
 
Biomechanics of Gait: Engineering Solutions for Rehabilitation (www.kiu.ac.ug)
publication11
 
Introduction to Productivity and Quality
মোঃ ফুরকান উদ্দিন জুয়েল
 
Pressure Measurement training for engineers and Technicians
AIESOLUTIONS
 

Ember testing internals with ember cli

  • 1. Ember Testing Internals with Ember-CLI Cory Forsyth @bantic
  • 2. 201 Created Matthew BealeCory Forsyth http://201-created.com/
  • 4. The Ember-CLI Testing Triumvirate • The test harness (tests/index.html) • Unit Test Affordances • Acceptance Test Affordances
  • 5. $ ember new my-app
  • 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">! <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
  • 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("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
  • 21. ember-qunit • imported via ember-cli-qunit addon • provides `moduleFor` • also: `moduleForModel`, `moduleForComponent` • provides `test`
  • 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
  • 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 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
  • 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 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()
  • 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: Cannot read property 'get' of undefined but not all the time
  • 49. Ember.Test.registerAsyncHelper('signIn', function(app) {! ! visit('/signin');! ! fillIn('input.email', '[email protected]');! ! 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) • `npm install —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
  • 54. Thank you Cory Forsyth @bantic Photo credits ! ! 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