55
Ember Testing Internals with Ember-CLI Cory Forsyth @bantic

Ember testing internals with ember cli

Embed Size (px)

DESCRIPTION

Ember Testing Internals with Ember-CLI

Citation preview

Page 1: Ember testing internals with ember cli

Ember Testing Internals with Ember-CLI

Cory Forsyth @bantic

Page 2: Ember testing internals with ember cli

201 Created

Matthew BealeCory Forsyth

http://201-created.com/

Page 3: Ember testing internals with ember cli

http://devopsreactions.tumblr.com/

Page 4: Ember testing internals with ember cli

The Ember-CLI Testing Triumvirate

• The test harness (tests/index.html)

• Unit Test Affordances

• Acceptance Test Affordances

Page 5: Ember testing internals with ember cli

$ ember new my-app

Page 6: Ember testing internals with ember cli
Page 7: 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

Page 8: Ember testing internals with ember cli

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)

Page 9: Ember testing internals with ember cli

<!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

Page 10: Ember testing internals with ember cli

<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

Page 11: Ember testing internals with ember cli

<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

Page 12: Ember testing internals with ember cli

/* 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

Page 13: Ember testing internals with ember cli

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

Page 14: Ember testing internals with ember cli

$ 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);!});!

Page 15: Ember testing internals with ember cli

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

Page 16: Ember testing internals with ember cli

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)

Page 17: Ember testing internals with ember cli

Ember-CLI Test Harness

Page 18: Ember testing internals with ember cli

Anatomy of a Unit Test

• How does Ember actually run a unit test?

• What does that boilerplate do?

Page 19: Ember testing internals with ember cli

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

Page 20: Ember testing internals with ember cli

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

Page 21: Ember testing internals with ember cli

ember-qunit• imported via ember-cli-qunit addon

• provides `moduleFor`

• also: `moduleForModel`, `moduleForComponent`

• provides `test`

Page 22: Ember testing internals with ember cli

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

Page 23: Ember testing internals with ember cli

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

Page 24: Ember testing internals with ember cli

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

Page 25: Ember testing internals with ember cli

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!

Page 26: Ember testing internals with ember cli

ember-cli-mocha

Page 27: Ember testing internals with ember cli

Ember Testing Affordances• Two primary types of tests in Ember:

• Unit Tests

• need isolated containers, specific setup

• use moduleFor

Page 28: Ember testing internals with ember cli

Ember Testing Affordances

• Two primary types of tests in Ember:

• Unit Tests and

• Acceptance Tests

• Totally different animal

• must manage async, interact with DOM

Page 29: Ember testing internals with ember cli

Ember Acceptance Tests

$ ember g acceptance-test index

Page 30: Ember testing internals with ember cli

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

Page 31: Ember testing internals with ember cli

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?

Page 32: Ember testing internals with ember cli

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

Page 33: Ember testing internals with ember cli

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

Page 34: Ember testing internals with ember cli

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

Page 35: Ember testing internals with ember cli

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

Page 36: Ember testing internals with ember cli

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

Page 37: Ember testing internals with ember cli

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

Page 38: Ember testing internals with ember cli

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

Page 39: Ember testing internals with ember cli

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

Page 40: Ember testing internals with ember cli

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

Page 41: Ember testing internals with ember cli

TimelineTest.lastPromise

Code

visit(‘/posts’); fillIn(‘input’); click(‘.submit’);

.then .then .then

visit(‘/posts’);

fillIn(‘input’);

click(‘.submit’);

magic ember async chaining

Page 42: Ember testing internals with ember cli

Ember Sync Test Helpers• Used for inspecting app state or DOM

• find(selector) — just like jQuery(selector)

• currentPathName()

• currentRouteName()

• currentURL()

• pauseTest() — new!

Page 43: Ember testing internals with ember cli

Ember Async Test Helpers• visit(url)

• fillIn(selector, text)

• click(selector)

• keyEvent(selector, keyCode)

• andThen(callback)

• wait() — this one is special

Page 44: Ember testing internals with ember cli

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()`

Page 45: Ember testing internals with ember cli

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()

Page 46: Ember testing internals with ember cli

A good test & framework

should guide you

Page 47: Ember testing internals with ember cli

visit(‘/foo’) The URL '/foo' did not match any routes …

click(‘input.button’) Element input.button not found.

Error messages can guide you, sometimes

Page 48: Ember testing internals with ember cli

? TypeError: Cannot read property 'get' of undefined

but not all the time

Page 49: Ember testing internals with ember cli

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

Page 50: Ember testing internals with ember cli

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

Page 51: Ember testing internals with ember cli

• (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

Page 52: Ember testing internals with ember cli

• 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

Page 53: Ember testing internals with ember cli

http://devopsreactions.tumblr.com/

testing your own code

doesn’t have to be like this

Page 54: Ember testing internals with ember cli

Thank youCory Forsyth

@bantic

Photo credits ! ! http://devopsreactions.tumblr.com/!www.ohmagif.com