Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Unofficial, Official Ember Testing Guide

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

The Unofficial, Official Ember Testing Guide

Presented at EmberConf 2014

Avatar for Eric Berry

Eric Berry

March 26, 2014

More Decks by Eric Berry

Other Decks in Technology

Transcript

  1. 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. "SJSA Grade Six - The Year I Rebelled" by Michael

    1952 attrib: inspiredMonkey.com
  3. 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');! });
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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()
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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');
  16. 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');!
  17. 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])
  18. 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’)
  19. 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’)
  20. 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()
  21. 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
  22. BDD

  23. 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
  24. 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);! })! });!
  25. 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);! })! });!
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. Container test(‘test 1’, function() {! var route = this.subject();! })

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

    test(‘test 1’, function() {! var route = this.subject();! }) moduleFor(“route:index”)
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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]])
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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]])
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. thank you @rwjblue @stefanpenner @abuiles @dericabel @tehviking @ryanflorence @fivetanley @jason.madsen

    @sterlingcobb @codeofficer @JimDAlvado @ryankshaw @DevEngine @toranb
  62. 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?