The Unofficial, Official Ember Testing Guide

The Unofficial, Official Ember Testing Guide

Presented at EmberConf 2014

0e2948f19a7dacdf4085d7d33093f260?s=128

Eric Berry

March 26, 2014
Tweet

Transcript

  1. 2.

    I tweet at @cavneb @coderberry I blog at coderberry.me I

    commit to github.com/cavneb I work for Instructure I run the EmberSLC meetup My cat can eat a whole watermelon Eric Berry
  2. 5.

    "SJSA Grade Six - The Year I Rebelled" by Michael

    1952 attrib: inspiredMonkey.com
  3. 6.
  4. 8.
  5. 12.
  6. 13.

    jsbin.com/suteg 1! 2! 3! 4! 5! 6! 7! 8! 9!

    10! 11! 12! 13 module('group of tests', {! setup: function() {! // run before each test! },! teardown: function() {! // run after each test! }! });! ! test('should be true', function() {! ok(true, 'should be true');! equal(1, 1, 'should equal');! });
  7. 14.

    qunit-basics.js 1! 2! 3! 4! 5! 6! 7! 8! 9!

    10! 11! 12! 13 module('group of tests', {! setup: function() {! // run before each test! },! teardown: function() {! // run after each test! }! });! ! test('should be true', function() {! ok(true, 'should be true');! equal(1, 1, 'should equal');! }); callbacks
  8. 15.
  9. 16.
  10. 17.
  11. 21.
  12. 22.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 App.setupForTesting();! App.injectTestHelpers();! ! test(‘welcome page', function() {! visit('/');! ! andThen(function() {! equal(find('h2').text(), 'Welcome to Ember.js');! equal(find('ul li').length, 3);! });! }); jsbin.com/suteg
  13. 23.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 App.setupForTesting();! App.injectTestHelpers();! ! test(‘welcome page', function() {! visit('/');! ! andThen(function() {! equal(find('h2').text(), 'Welcome to Ember.js');! equal(find('ul li').length, 3);! });! }); jsbin.com/suteg
  14. 24.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 App.setupForTesting();! App.injectTestHelpers();! ! test(‘welcome page', function() {! visit('/');! ! andThen(function() {! equal(find('h2').text(), 'Welcome to Ember.js');! equal(find('ul li').length, 3);! });! }); jsbin.com/suteg
  15. 25.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 App.setupForTesting();! App.injectTestHelpers();! ! test(‘welcome page', function() {! visit('/');! ! andThen(function() {! equal(find('h2').text(), 'Welcome to Ember.js');! equal(find('ul li').length, 3);! });! }); jsbin.com/suteg
  16. 32.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10

    test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz visit() fillIn() click() andThen() find() keyEvent()
  17. 33.

    visit() fillIn() click() andThen() find() keyEvent() 1! 2! 3! 4!

    5! 6! 7! 8! 9! 10 test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz
  18. 34.

    visit() fillIn() click() andThen() find() keyEvent() 1! 2! 3! 4!

    5! 6! 7! 8! 9! 10 test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz
  19. 35.

    visit() fillIn() click() andThen() find() keyEvent() 1! 2! 3! 4!

    5! 6! 7! 8! 9! 10 test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz
  20. 36.

    visit() fillIn() click() andThen() find() keyEvent() 1! 2! 3! 4!

    5! 6! 7! 8! 9! 10 test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz
  21. 37.

    visit() fillIn() click() andThen() find() keyEvent() 1! 2! 3! 4!

    5! 6! 7! 8! 9! 10 test('add new post', function() {! visit('/posts/new');! fillIn('input.title', 'My new post');! click('button.submit');! ! andThen(function() {! equal(find('ul.posts li:last').text(), ! 'My new post');! });! }); jsbin.com/vusaz
  22. 40.

    registerHelper(name, func, [options]) 1! 2! 3! 4! 5! 6! 7!

    8! 9! 10 Ember.Test.registerHelper('currentURL', ! function(app) {! return app.__container__.lookup('router:main')! .get('location')! .getURL()! }! );! ! // runs instantly! equal(currentUrl(), '/posts/new');
  23. 41.

    registerAsyncHelper(name, func, [options]) 1! 2! 3! 4! 5! 6! 7!

    8! 9! 10! 11 Ember.Test.registerAsyncHelper('submitForm',! function(app, selector, context) {! var $el = findWithAssert(selector, context);! Ember.run(function() {! $el.submit();! });! }! );! ! // usage! submitForm('.my-form');!
  24. 44.
  25. 45.
  26. 48.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12 test('trigger events', function() {! ! // simulate enter key! triggerEvent('#name', 'keypress', { keyCode: 13 });! ! // blur element! triggerEvent('#search', 'blur');! ! // double-click element! triggerEvent('.post-5', 'dblclick');
 ! });! ! triggerEvent(selector, type, [options])
  27. 49.

    1! 2! 3! 4! 5! 6! 7! 8! 9 test('root

    path goes to posts.index', function() {! visit('/');! andThen(function() {! ! // assert we made it to the correct route! equal(currentRouteName(), 'posts.index');! ! });! });! currentRouteName() App.__container__.lookup(‘controllers:application’).get(’currentRouteName’)
  28. 50.

    1! 2! 3! 4! 5! 6! 7! 8! 9 test('redirect

    occurred', function() {! visit('/posts/new');! andThen(function() {! ! // assert we made it to the correct route! equal(currentPath(), 'posts.new');! ! });! });! currentPath() App.__container__.lookup(‘controllers:application’).get(’currentPath’)
  29. 51.

    1! 2! 3! 4! 5! 6! 7! 8! 9 test('redirect

    occurred', function() {! visit('/posts/new');! andThen(function() {! ! // assert we made it to the correct route! equal(currentURL(), '/posts/new');! ! });! });! currentURL() App.__container__.lookup(‘router:main’).get(’location’).getURL()
  30. 53.
  31. 54.
  32. 55.

    Scenario 1: A list of posts should be filterable by

    title Given there are 3 posts When I visit ‘/posts’ And enter filter text which matches 1 post Then I should see 1 post
  33. 56.

    BDD

  34. 58.

    Scenario 1: A list of posts should be filterable by

    title Given there are 3 posts When I visit ‘/posts’ And enter filter text which matches 1 post Then I should see 1 post
  35. 59.

    jsbin.com/cahuc 1! 2! 3! 4! 5! 6! 7 test("filters posts

    by title", function() {! visit("/posts");! fillIn(".search", "auth");! andThen(function() {! equal(find(".post-title").length, 1);! })! });!
  36. 60.

    jsbin.com/cahuc 1! 2! 3! 4! 5! 6! 7 test("filters posts

    by title", function() {! visit("/posts");! fillIn(".search", "auth");! andThen(function() {! equal(find(".post-title").length, 1);! })! });! 1! 2! 3! 4! 5! 6! 7 test("filters users by name", function() {! visit("/users");! fillIn(".search", “Bob Hanson");! andThen(function() {! equal(find(".user-name").length, 1);! })! });!
  37. 61.

    1! 2! 3! 4! 5 App.PostsController = Ember.ArrayController.extend(! App.FilterableArrayMixin, {!

    ! filterField: 'title'! });! 1! 2! 3! 4! 5 App.UsersController = Ember.ArrayController.extend(! App.FilterableArrayMixin, {! ! filterField: 'name'! });! jsbin.com/cahuc
  38. 62.

    1! 2! 3! 4! 5 App.PostsController = Ember.ArrayController.extend(! App.FilterableArrayMixin, {!

    ! filterField: 'title'! });! 1! 2! 3! 4! 5 App.UsersController = Ember.ArrayController.extend(! App.FilterableArrayMixin, {! ! filterField: 'name'! });! jsbin.com/cahuc
  39. 63.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! 13! 14! 15 App.FilterableArrayMixin = Ember.Mixin.create({! filterText: null,! ! filteredContent: function() {! var filterText = this.getWithDefault('filterText', '');! if (Em.isEmpty(filterText)) {! return this.get('content');! } else {! return this.get('content').filter(function(item) {! var str = item.get(this.get('filterField')).toLowerCase();! return str.match(filterText.toLowerCase());! }.bind(this));! }! }.property('content', 'filterText')! });! jsbin.com/cahuc
  40. 64.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! 13! 14! 15 App.FilterableArrayMixin = Ember.Mixin.create({! filterText: null,! ! filteredContent: function() {! var filterText = this.getWithDefault('filterText', '');! if (Em.isEmpty(filterText)) {! return this.get('content');! } else {! return this.get('content').filter(function(item) {! var str = item.get(this.get('filterField')).toLowerCase();! return str.match(filterText.toLowerCase());! }.bind(this));! }! }.property('content', 'filterText')! });! jsbin.com/cahuc
  41. 65.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! 13! 14! 15 App.FilterableArrayMixin = Ember.Mixin.create({! filterText: null,! ! filteredContent: function() {! var filterText = this.getWithDefault('filterText', '');! if (Em.isEmpty(filterText)) {! return this.get('content');! } else {! return this.get('content').filter(function(item) {! var str = item.get(this.get('filterField')).toLowerCase();! return str.match(filterText.toLowerCase());! }.bind(this));! }! }.property('content', 'filterText')! });! ISOLATED? jsbin.com/cahuc
  42. 66.
  43. 67.
  44. 69.
  45. 71.
  46. 73.
  47. 74.
  48. 75.
  49. 80.
  50. 81.
  51. 82.

    globals-unit-test-setup.js 1! 2! 3! 4! 5! 6! 7! 8 //

    inject test helpers onto window! emq.globalize();! ! // create a custom test resolver! App.Resolver = Ember.DefaultResolver.extend({ namespace: App });! ! // set the test resolver! setResolver(App.Resolver.create());! Globals
  52. 83.

    globals-unit-test-setup.js 1! 2! 3! 4! 5! 6! 7! 8 //

    inject test helpers onto window! emq.globalize();! ! // create a custom test resolver! App.Resolver = Ember.DefaultResolver.extend({ namespace: App });! ! // set the test resolver! setResolver(App.Resolver.create());! Globals
  53. 84.

    globals-unit-test-setup.js 1! 2! 3! 4! 5! 6! 7! 8 //

    inject test helpers onto window! emq.globalize();! ! // create a custom test resolver! App.Resolver = Ember.DefaultResolver.extend({ namespace: App });! ! // set the test resolver! setResolver(App.Resolver.create());! Globals
  54. 85.

    modules-unit-test-setup.js 1! 2! 3! 4! 5! 6! 7! 8! 9!

    10! 11 // inject test helpers onto window! emq.globalize();! ! // import the existing resolver! import Resolver from ‘./path/to/resolver';! ! // import the setResolver function! import { setResolver } from ‘ember-qunit';! ! // set the resolver! setResolver(Resolver.create());! Modules
  55. 86.

    modules-unit-test-setup.js 1! 2! 3! 4! 5! 6! 7! 8! 9!

    10! 11 // inject test helpers onto window! emq.globalize();! ! // import the existing resolver! import Resolver from ‘./path/to/resolver';! ! // import the setResolver function! import { setResolver } from ‘ember-qunit';! ! // set the resolver! setResolver(Resolver.create());! Modules
  56. 93.

    Container test(‘test 1’, function() {! var route = this.subject();! })

    test(‘test 1’, function() {! var route = this.subject();! }) moduleFor(“route:index”)
  57. 94.

    Container test(‘test 1’, function() {! var route = this.subject();! })

    test(‘test 1’, function() {! var route = this.subject();! }) moduleFor(“route:index”)
  58. 95.

    moduleFor(fullName [, description [, callbacks]]) The description of the module

    description The full name of the unit (ie. controller:application or route:index) fullName Normal QUnit callbacks (setup, teardown), width addition of needs callbacks
  59. 96.
  60. 97.

    1! 2! 3! 4! 5! App.IndexRoute = Ember.Route.extend({! model: function()

    {! return ['red', 'green', 'blue'];! }! });! 1! 2! 3! 4! 5! 6 moduleFor('route:index', 'Unit: Index Route');! ! test('model', function() {! var route = this.subject();! equal(route.model().toString(), 'red,green,blue');! });! jsbin.com/qifon
  61. 98.

    1! 2! 3! 4! 5! App.IndexRoute = Ember.Route.extend({! model: function()

    {! return ['red', 'green', 'blue'];! }! });! 1! 2! 3! 4! 5! 6 moduleFor('route:index', 'Unit: Index Route');! ! test('model', function() {! var route = this.subject();! equal(route.model().toString(), 'red,green,blue');! });! jsbin.com/qifon
  62. 99.

    1! 2! 3! 4! 5! App.IndexRoute = Ember.Route.extend({! model: function()

    {! return ['red', 'green', 'blue'];! }! });! 1! 2! 3! 4! 5! 6! moduleFor('route:index', 'Unit: Index Route');! ! test('model', function() {! var route = this.subject();! equal(route.model().toString(), 'red,green,blue');! });! jsbin.com/qifon
  63. 100.

    1! 2! 3! 4! 5! App.IndexRoute = Ember.Route.extend({! model: function()

    {! return ['red', 'green', 'blue'];! }! });! 1! 2! 3! 4! 5! 6 moduleFor('route:index', 'Unit: Index Route');! ! test('model', function() {! var route = this.subject();! equal(route.model().toString(), 'red,green,blue');! });! jsbin.com/qifon
  64. 102.

    1! 2! 3! 4! 5! 6! 7! 8! ! 1!

    2! 3! 4! 5! 6! 7! ! App.ApplicationController = Ember.Controller.extend({! user: null,! ! init: function() {! this._super.apply(this, arguments);! this.set('user', Em.Object.create({ name: 'Guest' }));! }! });! ! App.ProfileController = Ember.Controller.extend({! needs: ['application'],! ! name: function() {! return this.get('controllers.application.user.name');! }.property('controllers.application.user.name')! });! jsbin.com/numof
  65. 103.

    1! 2! 3! 4! 5! 6! 7! 8! ! 1!

    2! 3! 4! 5! 6! 7! ! App.ApplicationController = Ember.Controller.extend({! user: null,! ! init: function() {! this._super.apply(this, arguments);! this.set('user', Em.Object.create({ name: 'Guest' }));! }! });! ! App.ProfileController = Ember.Controller.extend({! needs: ['application'],! ! name: function() {! return this.get('controllers.application.user.name');! }.property('controllers.application.user.name')! });! jsbin.com/numof
  66. 104.

    1! 2! 3! 4! 5! 6! 7! 8! 9 !

    10! 11! 12! 13! 14! ! moduleFor('controller:profile', 'Unit: Stuff Controller', {! needs: ['controller:application']! });! ! test('name', function() {! var ctrl = this.subject(),! appCtrl = ctrl.get('controllers.application');! ! equal(ctrl.get('name'), 'Guest');! Ember.run(function() {! appCtrl.get('user').set('name', 'Eric');! });! equal(ctrl.get('name'), 'Eric');! });! jsbin.com/numof
  67. 105.

    1! 2! 3! 4! 5! 6! 7! 8! 9 !

    10! 11! 12! 13! 14! ! moduleFor('controller:profile', 'Unit: Stuff Controller', {! needs: ['controller:application']! });! ! test('name', function() {! var ctrl = this.subject(),! appCtrl = ctrl.get('controllers.application');! ! equal(ctrl.get('name'), 'Guest');! Ember.run(function() {! appCtrl.get('user').set('name', 'Eric');! });! equal(ctrl.get('name'), 'Eric');! });! jsbin.com/numof
  68. 106.

    1! 2! 3! 4! 5! 6! 7! 8! 9 !

    10! 11! 12! 13! 14! ! moduleFor('controller:stuff', 'Unit: Stuff Controller', {! needs: ['controller:application']! });! ! test('name', function() {! var ctrl = this.subject(),! appCtrl = ctrl.get('controllers.application');! ! equal(ctrl.get('name'), 'Guest');! Ember.run(function() {! appCtrl.get('user').set('name', 'Eric');! });! equal(ctrl.get('name'), 'Eric');! });! jsbin.com/numof
  69. 107.

    1! 2! 3! 4! 5! 6! 7! 8! 9 !

    10! 11! 12! 13! 14! ! moduleFor('controller:stuff', 'Unit: Stuff Controller', {! needs: ['controller:application']! });! ! test('name', function() {! var ctrl = this.subject(),! appCtrl = ctrl.get('controllers.application');! ! equal(ctrl.get('name'), 'Guest');! Ember.run(function() {! appCtrl.get('user').set('name', 'Eric');! });! equal(ctrl.get('name'), 'Eric');! });! jsbin.com/numof
  70. 108.

    The description of the module description The short name of

    the component (ie. x-foo or ic-tabs) name Normal QUnit callbacks (setup, teardown), width addition of needs callbacks moduleForComponent(name [, description [, callbacks]])
  71. 109.

    1! 2! 3! 4! 5! 6! 7! App.PrettyColorComponent = Ember.Component.extend({!

    classNames: ['pretty-color'],! attributeBindings: ['style'],! style: function() {! return 'color: ' + this.get('color') + ';';! }.property('color')! });! jsbin.com/witut
  72. 110.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! …! moduleForComponent('pretty-color', 'Unit: components/pretty-color');! ! test("set colors", function() {! var component = this.subject();! ! Ember.run(function() {! component.set('color', 'green');! });! ! // first call to this.$() renders the component! equal(this.$().attr('style'), 'color: green;');! }); jsbin.com/witut
  73. 111.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! …! moduleForComponent('pretty-color', 'Unit: components/pretty-color');! ! test("set colors", function() {! var component = this.subject();! ! Ember.run(function() {! component.set('color', 'green');! });! ! // first call to this.$() renders the component! equal(this.$().attr('style'), 'color: green;');! }); jsbin.com/witut
  74. 112.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! …! moduleForComponent('pretty-color', 'Unit: components/pretty-color');! ! test("set colors", function() {! var component = this.subject();! ! Ember.run(function() {! component.set('color', 'green');! });! ! // first call to this.$() renders the component! equal(this.$().attr('style'), 'color: green;');! }); jsbin.com/witut
  75. 113.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11! 12! …! moduleForComponent('pretty-color', 'Unit: components/pretty-color');! ! test("set colors", function() {! var component = this.subject();! ! Ember.run(function() {! component.set('color', 'green');! });! ! // first call to this.$() renders the component! equal(this.$().attr('style'), 'color: green;');! }); jsbin.com/witut
  76. 115.

    The description of the module description The short name of

    the model you’d use in `store` operations (ie. user or assignmentGroup) name Normal QUnit callbacks (setup, teardown), width addition of needs callbacks moduleForModel(name [, description [, callbacks]])
  77. 116.

    1! 2! 3! 4! 5! 6! 7! 8 App.User =

    DS.Model.extend({! fName: DS.attr('string'),! lName: DS.attr('string'),! verified: DS.attr('boolean', { defaultValue: false }),! createdAt: DS.attr('string', {! defaultValue: function() { return new Date(); }! })! }); jsbin.com/mapuf
  78. 117.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 moduleForModel('user', 'Unit: User Model');! ! test('createdAt defaults to now', function() {! var user = this.subject({! firstName: 'Eric',! lastName: 'Berry'! });! var createdAt = user.get('createdAt');! var now = new Date();! equal(createdAt.toString(), now.toString());! });! jsbin.com/mapuf
  79. 118.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 moduleForModel('user', 'Unit: User Model');! ! test('createdAt defaults to now', function() {! var user = this.subject({! firstName: 'Eric',! lastName: 'Berry'! });! var createdAt = user.get('createdAt');! var now = new Date();! equal(createdAt.toString(), now.toString());! });! jsbin.com/mapuf
  80. 119.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 moduleForModel('user', 'Unit: User Model');! ! test('createdAt defaults to now', function() {! var user = this.subject({! firstName: 'Eric',! lastName: 'Berry'! });! var createdAt = user.get('createdAt');! var now = new Date();! equal(createdAt.toString(), now.toString());! });! jsbin.com/mapuf
  81. 120.

    1! 2! 3! 4! 5! 6! 7! 8! 9! 10!

    11 moduleForModel('user', 'Unit: User Model');! ! test('createdAt defaults to now', function() {! var user = this.subject({! firstName: 'Eric',! lastName: 'Berry'! });! var createdAt = user.get('createdAt');! var now = new Date();! equal(createdAt.toString(), now.toString());! });! jsbin.com/mapuf
  82. 125.
  83. 127.

    thank you @rwjblue @stefanpenner @abuiles @dericabel @tehviking @ryanflorence @fivetanley @jason.madsen

    @sterlingcobb @codeofficer @JimDAlvado @ryankshaw @DevEngine @toranb
  84. 129.

    I tweet at @coderberry I blog at coderberry.me I commit

    to github.com/cavneb I work for Instructure I run the EmberSLC meetup I actually don’t even have a cat Questions?