Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Ember testing internals with ember cli

Ember Testing Internals with Ember-CLI

  • Be the first to comment

Ember testing internals with ember cli

  1. 1. Ember Testing Internals with Ember-CLI Cory Forsyth @bantic
  2. 2. 201 Created Matthew BealeCory Forsyth http://201-created.com/
  3. 3. http://devopsreactions.tumblr.com/
  4. 4. The Ember-CLI Testing Triumvirate • The test harness (tests/index.html) • Unit Test Affordances • Acceptance Test Affordances
  5. 5. $ ember new my-app
  6. 6. 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
  7. 7. 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)
  8. 8. <!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
  9. 9. <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
  10. 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>! 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
  11. 11. /* 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
  12. 12. 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
  13. 13. $ 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);! });!
  14. 14. 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
  15. 15. 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)
  16. 16. Ember-CLI Test Harness
  17. 17. Anatomy of a Unit Test • How does Ember actually run a unit test? • What does that boilerplate do?
  18. 18. 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
  19. 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. 20. ember-qunit • imported via ember-cli-qunit addon • provides `moduleFor` • also: `moduleForModel`, `moduleForComponent` • provides `test`
  21. 21. 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
  22. 22. 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
  23. 23. 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
  24. 24. 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!
  25. 25. ember-cli-mocha
  26. 26. Ember Testing Affordances • Two primary types of tests in Ember: • Unit Tests • need isolated containers, specific setup • use moduleFor
  27. 27. Ember Testing Affordances • Two primary types of tests in Ember: • Unit Tests and • Acceptance Tests • Totally different animal • must manage async, interact with DOM
  28. 28. Ember Acceptance Tests $ ember g acceptance-test index
  29. 29. 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
  30. 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 What if visiting / takes 5 seconds? How does this know to wait?
  31. 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');! });! });! What if visiting / takes 5 seconds? How does this know to wait? tests/unit/controllers/index-test.js
  32. 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');! });! });! vanilla QUnit module tests/acceptance/index-test.js
  33. 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 special test helpers: visit, andThen, currentPath tests/acceptance/index-test.js
  34. 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');! });! });! What is `startApp`? tests/acceptance/index-test.js
  35. 35. 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
  36. 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;! }! • set Ember.testing = true • set a test adapter • prep for ajax: • listeners for ajaxSend, ajaxComplete tests/helpers/start_app.js
  37. 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;! }! • wrap all registered test helpers • 2 types: sync and async tests/helpers/start_app.js
  38. 38. 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
  39. 39. 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
  40. 40. Timeline Test.lastPromise Code visit(‘/posts’); fillIn(‘input’); click(‘.submit’); .then .then .then visit(‘/posts’); fillIn(‘input’); click(‘.submit’); magic ember async chaining
  41. 41. Ember Sync Test Helpers • Used for inspecting app state or DOM • find(selector) — just like jQuery(selector) • currentPathName() • currentRouteName() • currentURL() • pauseTest() — new!
  42. 42. Ember Async Test Helpers • visit(url) • fillIn(selector, text) • click(selector) • keyEvent(selector, keyCode) • andThen(callback) • wait() — this one is special
  43. 43. 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()`
  44. 44. 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()
  45. 45. A good test & framework should guide you
  46. 46. visit(‘/foo’) The URL '/foo' did not match any routes … click(‘input.button’) Element input.button not found. Error messages can guide you, sometimes
  47. 47. ? TypeError: Cannot read property 'get' of undefined but not all the time
  48. 48. 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
  49. 49. 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
  50. 50. • (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
  51. 51. • 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
  52. 52. http://devopsreactions.tumblr.com/ testing your own code doesn’t have to be like this
  53. 53. Thank you Cory Forsyth @bantic Photo credits ! ! http://devopsreactions.tumblr.com/! www.ohmagif.com
  54. 54. 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

×