Twig internals - Maksym MoskvychevTwig internals maksym moskvychev
Ember testing internals with ember cli
-
Upload
cory-forsyth -
Category
Engineering
-
view
9.910 -
download
6
description
Transcript of Ember testing internals with ember cli
Ember Testing Internals with Ember-CLI
Cory Forsyth @bantic
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
TimelineTest.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', '[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
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 youCory 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