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

The Unofficial, Official Ember Testing Guide

The Unofficial, Official Ember Testing Guide

Presented at EmberConf 2014

Eric Berry

March 26, 2014
Tweet

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?