Slide 1

Slide 1 text

The Next Generation of Testing in __

Slide 2

Slide 2 text

Turbo87 TobiasBieniek

Slide 3

Slide 3 text

simplabs based in Munich consulting all over ! * Stickers available after the talk

Slide 4

Slide 4 text

Ember CLI

Slide 5

Slide 5 text

⏰ andThen() The andThen helper will wait for all preceding asynchronous helpers to complete prior to progressing forward.

Slide 6

Slide 6 text

andThen() import { test } from 'qunit'; import moduleForAcceptance from 'my-app/tests/helpers/module-for-acceptance'; moduleForAcceptance('Acceptance | posts'); test('should add new post', function(assert) { visit('/posts/new'); fillIn('input.title', 'My new post'); click('button.submit'); andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post')); });

Slide 7

Slide 7 text

andThen() import { test } from 'qunit'; import moduleForAcceptance from 'my-app/tests/helpers/module-for-acceptance'; moduleForAcceptance('Acceptance | posts'); test('should add new post', function(assert) { return visit('/posts/new').then(function() { return fillIn('input.title', 'My new post'); }).then(function() { return click('button.submit'); }).then(function() { assert.equal(find('ul.posts li:first').text(), 'My new post') }); }); April 2013

Slide 8

Slide 8 text

andThen() vs. async /await import { test } from 'qunit'; import moduleForAcceptance from 'my-app/tests/helpers/module-for-acceptance'; moduleForAcceptance('Acceptance | posts'); test('should add new post', async function(assert) { await visit('/posts/new'); await fillIn('input.title', 'My new post'); await click('button.submit'); assert.equal(find('ul.posts li:first').text(), 'My new post'); });andThen

Slide 9

Slide 9 text

andThen() vs. async /await andThen() • almost 5 years old • mixes sync and async code • unintuitive API async / await • standardized ECMAScript • explicit async code • Promise-based

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

30 kB no longer needed
 by Ember.js itself

Slide 12

Slide 12 text

30 kB still needed by
 the test helpers

Slide 13

Slide 13 text

jQuery await click('button.submit'); assert.equal(find('ul.posts li').text(), 'My new post'); this.$('button.submit').click(); assert.equal(this.$('ul.posts li').text(), 'My new post'); jQuery jQuery jQuery Acceptance Component / Integration

Slide 14

Slide 14 text

jQuery await click('button.submit'); assert.equal(find('ul.posts li').text(), 'My new post');

Slide 15

Slide 15 text

jQuery import { click, find } from 'ember-native-dom-helpers'; await click('button.submit'); assert.equal(find('ul.posts li').textContent, 'My new post');

Slide 16

Slide 16 text

jQuery import { click, find } from '@ember/test-helpers'; await click('button.submit'); assert.equal(find('ul.posts li').textContent, 'My new post');

Slide 17

Slide 17 text

Nested Modules

Slide 18

Slide 18 text

Nested Modules describe('Galaxy', function() { describe('Earth', function() { describe('Portland', function() { it('is awesome at EmberConf', function() { // right? });aaa }); }); });

Slide 19

Slide 19 text

Nested Modules describe('Galaxy', function() {hooks describe('Earth', function() {hooks beforeEach(function() {hooks this.owner.lookup('service:time').setYear(2018); }); describe('Portland', function() {hooks it('is awesome at EmberConf', function() {assert // right? });aaa }); }); });

Slide 20

Slide 20 text

Nested Modules module('Galaxy', function() { module('Earth', function(hooks) { hooks.beforeEach(function() { this.owner.lookup('service:time').setYear(2018); }); module('Portland', function() { test('is awesome at EmberConf', function(assert) { // right? });aaa }); }); });

Slide 21

Slide 21 text

Nested Modules module('Galaxy', function(hooks) { module('Earth', function(hooks) { hooks.beforeEach(function() { this.owner.lookup('service:time').setYear(2018); }); module('Portland', function(hooks) { test('is awesome at EmberConf', function(assert) { // right? });aaa }); }); }); Not compatible with • moduleFor() • moduleForComponent() • moduleForModel() • moduleForAcceptance()

Slide 22

Slide 22 text

The Grand Testing Unification

Slide 23

Slide 23 text

RFC #119

Slide 24

Slide 24 text

RFC #119

Slide 25

Slide 25 text

RFC #232

Slide 26

Slide 26 text

RFC #232 Replaces • moduleFor() • moduleForComponent() • moduleForModel() with • module() + setupTest()

Slide 27

Slide 27 text

RFC #268

Slide 28

Slide 28 text

RFC #268 Replaces • moduleForAcceptance() with • module() + setupApplicationTest()

Slide 29

Slide 29 text

Plain QUnit tests for code unrelated to Ember

Slide 30

Slide 30 text

Plain QUnit tests import { module, test } from 'qunit'; module('relativeDate', function(hooks) { test('format relative dates correctly', function(assert) { assert.equal(relativeDate('2018/01/28 22:24:30'), 'just now'); assert.equal(relativeDate('2018/01/28 22:23:30'), '1 minute ago'); assert.equal(relativeDate('2018/01/28 21:23:30'), '1 hour ago'); assert.equal(relativeDate('2018/01/27 22:23:30'), 'Yesterday'); assert.equal(relativeDate('2018/01/26 22:23:30'), '2 days ago'); }); }); regular QUnit APIs

Slide 31

Slide 31 text

"Container" tests for Controllers, Routes, Services, ... (previously Unit / Integration)

Slide 32

Slide 32 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Service | flash-messages', function(hooks) { setupTest(hooks); test('it buffers messages', function(assert) { let service = this.owner.lookup('service:flash-messages'); service.add('Hello'); service.add('World!'); assert.deepEqual(service.get('messages'), ['Hello', 'World!']); }); }); setupTest() regular QUnit APIs ember-qunit APIs

Slide 33

Slide 33 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Service | flash-messages', function(hooks) { setupTest(hooks); test('it buffers messages', function(assert) { let service = this.owner.lookup('service:flash-messages'); service.add('Hello'); service.add('World!'); assert.deepEqual(service.get('messages'), ['Hello', 'World!']); }); }); setupTest() Sets up the Testing Container

Slide 34

Slide 34 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Service | flash-messages', function(hooks) { setupTest(hooks); test('it buffers messages', function(assert) { let service = this.owner.lookup('service:flash-messages'); service.add('Hello'); service.add('World!'); assert.deepEqual(service.get('messages'), ['Hello', 'World!']); }); }); setupTest() Direct Container Access

Slide 35

Slide 35 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Service | flash-messages', function(hooks) { setupTest(hooks); test('it buffers messages', function(assert) { let service = this.owner.lookup('service:flash-messages'); service.add('Hello'); service.add('World!'); assert.deepEqual(service.get('messages'), ['Hello', 'World!']); }); }); setupTest()

Slide 36

Slide 36 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Service | flash-messages', function(hooks) { setupTest(hooks); test('it buffers messages', function(assert) { let service = this.owner.lookup('service:flash-messages'); service.add('Hello'); service.add('World!'); assert.deepEqual(service.get('messages'), ['Hello', 'World!']); }); }); setupTest()

Slide 37

Slide 37 text

Rendering tests for Components and Helpers (previously Component-Integration)

Slide 38

Slide 38 text

import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; module('Component | counter', function(hooks) { setupRenderingTest(hooks); setupRenderingTest() Shared between
 QUnit and Mocha

Slide 39

Slide 39 text

module('Component | counter', function(hooks) { setupRenderingTest(hooks); test('it should count clicks', async function(assert) { this.set('value', 0); await render(hbs`{{x-counter value=value onUpdate=( ...)}}`); assert.equal(this.element.textContent, '0 clicks'); await click('.counter'); assert.equal(this.element.textContent, '1 click'); }); }); setupRenderingTest() similar to setupTest()

Slide 40

Slide 40 text

module('Component | counter', function(hooks) { setupRenderingTest(hooks); test('it should count clicks', async function(assert) { this.set('value', 0); await render(hbs`{{x-counter value=value onUpdate=( ...)}}`); assert.equal(this.element.textContent, '0 clicks'); await click('.counter'); assert.equal(this.element.textContent, '1 click'); }); }); setupRenderingTest()

Slide 41

Slide 41 text

Application tests to verify user stories (previously Acceptance)

Slide 42

Slide 42 text

import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { visit, fillIn, click } from '@ember/test-helpers'; module('Acceptance | posts', function(hooks) { setupApplicationTest(hooks); test('should add new post', async function(assert) { await visit('/posts/new'); await fillIn('input.title', 'My new post'); await click('button.submit'); let title = this.element.querySelector('ul.posts li:first').textContent; assert.equal(title, 'My new post'); }); }); setupApplicationTest() same helpers as for
 rendering tests

Slide 43

Slide 43 text

import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { visit, fillIn, click } from '@ember/test-helpers'; module('Acceptance | posts', function(hooks) { setupApplicationTest(hooks); test('should add new post', async function(assert) { await visit('/posts/new'); await fillIn('input.title', 'My new post'); await click('button.submit'); let title = this.element.querySelector('ul.posts li:first').textContent; assert.equal(title, 'My new post'); }); }); setupApplicationTest() similar to setupRenderingTest()

Slide 44

Slide 44 text

import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { visit, fillIn, click } from '@ember/test-helpers'; module('Acceptance | posts', function(hooks) { setupApplicationTest(hooks); test('should add new post', async function(assert) { await visit('/posts/new'); await fillIn('input.title', 'My new post'); await click('button.submit'); let title = this.element.querySelector('ul.posts li:first').textContent; assert.equal(title, 'My new post'); }); }); setupApplicationTest()

Slide 45

Slide 45 text

ember install ???

Slide 46

Slide 46 text

ember install
 ember-cli-qunit 4.2.0+

Slide 47

Slide 47 text

Migration What about the 5000 tests we already have?

Slide 48

Slide 48 text

Migration async/await + ember-native-dom-helpers , ember-native-dom-helpers-codemod

Slide 49

Slide 49 text

ember-qunit-codemod Migration setupTest() functions .

Slide 50

Slide 50 text

ember-test-helpers-codemod Migration @ember/test-helpers /

Slide 51

Slide 51 text

Tips & Tricks ✨ ✨

Slide 52

Slide 52 text

Mocking aka. taking advantage of inject()

Slide 53

Slide 53 text

Mocking test('initializes with the cookie value', function() { this.owner.register('service:cookies', Service.extend({ read(key) { return 'a;b;c'; }, })); let featuresService = this.owner.lookup('service:features'); assert.deepEqual(featuresService.get('features'), ['a', 'b', 'c']); }); Register Mock Object

Slide 54

Slide 54 text

Mocking test('initializes with the cookie value', function() { this.owner.register('service:cookies', Service.extend({ read(key) { return 'a;b;c'; } })); let featuresService = this.owner.lookup('service:features'); assert.deepEqual(featuresService.get('features'), ['a', 'b', 'c']); }); Lookup Object using the Mock

Slide 55

Slide 55 text

Mocking test('initializes with the cookie value', function() { this.owner.register('service:cookies', Service.extend({ read(key) { return 'a;b;c'; } })); let featuresService = this.owner.lookup('service:features'); assert.deepEqual(featuresService.get('features'), ['a', 'b', 'c']); });

Slide 56

Slide 56 text

Mocking test('initializes with the cookie value', function() { this.owner.register('service:cookies', Service.extend({ read(key) { return 'a;b;c'; } })); let featuresService = this.owner.lookup('service:features'); assert.deepEqual(featuresService.get('features'), ['a', 'b', 'c']); });

Slide 57

Slide 57 text

Loading States How can we test them?

Slide 58

Slide 58 text

Loading States test('shows loading spinner after submitting', async function() { await visit('/comments/new'); await fillIn('input.comment', 'I ❤ Ember.js'); let promise = click('.submit'); await waitFor('.loading-spinner'); await promise; assert.ok(find('.you-for-submitting')); }); Save Promise for later

Slide 59

Slide 59 text

Loading States test('shows loading spinner after submitting', async function() { await visit('/comments/new'); await fillIn('input.comment', 'I ❤ Ember.js'); let promise = click('.submit'); await waitFor('.loading-spinner'); await promise; assert.ok(find('.you-for-submitting')); }); "Wait for loading spinner"

Slide 60

Slide 60 text

Loading States test('shows loading spinner after submitting', async function() { await visit('/comments/new'); await fillIn('input.comment', 'I ❤ Ember.js'); let promise = click('.submit'); await waitFor('.loading-spinner'); await promise; assert.ok(find('.you-for-submitting')); }); Cleanup and final assertions

Slide 61

Slide 61 text

Loading States await waitFor('.loading-spinner'); same as: await waitUntil(() => find('.loading-spinner'));

Slide 62

Slide 62 text

Custom Test Helpers aka. registerAsyncHelper()

Slide 63

Slide 63 text

Custom Test Helpers export default registerAsyncHelper('addContact', function(app, name) { fillIn('#name', name); click('button.create'); }); export async function addContact(name) { await fillIn('#name', name); await click('button.create'); }

Slide 64

Slide 64 text

Test Selectors the thing you know from CSS
 that helps you find elements in the DOM

Slide 65

Slide 65 text

Test Selectors

{{title}}

await fillIn('.title-field', 'Hello World!'); let title = this.element.querySelector('h1').textContent; assert.equal(title, 'Hello World!'); Test Selectors

Slide 66

Slide 66 text

Test Selectors

{{title}}

await fillIn('[data-test-title-field]', 'Hello World!'); let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title, 'Hello World!');

Slide 67

Slide 67 text

Test Selectors

{{title}}

await fillIn('[data-test-title-field]', 'Hello World!'); let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title, 'Hello World!'); ember install ember-test-selectors https://github.com/simplabs/ember-test-selectors

Slide 68

Slide 68 text

Readable Assertions How to write code that is easy to understand

Slide 69

Slide 69 text

Readable Assertions let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title.trim(), 'Hello World!');

Slide 70

Slide 70 text

Readable Assertions let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title.trim(), 'Hello World!'); assert.dom('[data-test-title]').hasText('Hello World!');

Slide 71

Slide 71 text

ember install qunit-dom https://github.com/simplabs/qunit-dom assert.dom('h1').exists(); assert.dom('h1').hasClass('title'); assert.dom('h1').hasText('Welcome to Ember, John Doe!'); assert.dom('input').isFocused(); assert.dom('input').hasValue(/.+ Doe/); assert.dom('input').hasAttribute('type', 'text');

Slide 72

Slide 72 text

the Ember testing ecosystem ember-exam ember-cli-eslint ember-cli-code-coverage ember-data-factory-guy ember-native-dom-helpers ember-mirage ember-try ember-cli-template-lint

Slide 73

Slide 73 text

Thanks