Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
The Next Generation of Testing in Ember.js
Search
Tobias Bieniek
March 13, 2018
Technology
1k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
The Next Generation of Testing in Ember.js
Tobias Bieniek
March 13, 2018
More Decks by Tobias Bieniek
See All by Tobias Bieniek
Please wait… Oh, it didn't work!
turbo87
0
240
Abstract Syntax Forestry
turbo87
0
73
Help! How do you let others know what’s going on when you’re 8000 feet up in the air in a plane without an engine?!
turbo87
0
140
ELS – The Ember Language Server
turbo87
0
270
Internationali[sz]ation: It's easy in Ember!
turbo87
1
430
EuregioCup 2018 - Selbstbriefing
turbo87
1
77
750 km FAI-Dreieck mit der Clubklasse
turbo87
3
81
High Level DOM Assertions for QUnit
turbo87
0
140
EuregioCup 2017 - Selbstbriefing
turbo87
1
61
Other Decks in Technology
See All in Technology
事業価値を⽣み出すSREへ SREが担うべき意思決定の5層
kenta_hi
2
3.6k
技術イベント終了後、運営の 事後タスクは丁寧に (心がけています)/ #tamagawadev
nishiuma
1
100
AI Driven AI Governance
pict3
0
350
「ちゃんとやっている」は独りよがりだった ― 不安に寄り添うインシデント対応へ / Towards incident response that addresses anxieties
chmikata
1
5.1k
Devsumi 2026 Summer 人もAIも使える共通基盤を事業の加速装置にする~デザインシステム運用に学ぶ組織レバレッジ~ 渡辺 凌央
legalontechnologies
PRO
0
120
10年目を迎えた「ABEMA」がどのように AI 活用を推進して、AI 駆動開発にシフトしているのか / How ABEMA, entering its 10th year, is promoting the use of AI and shifting toward AI-driven development
miyukki
0
130
証券システムを10年Scalaで作り続けるということ - 関数型まつり2026
krrrr38
3
840
Compose 新機能総まとめ / What's New in Jetpack Compose
yanzm
0
170
完全自律ロボットを作りたくて、先に開発を自律させた話(ROS Japan UG #63 LT)
rryz09
0
510
キャリアの中で本を作る / Making a Book During Your Career
ak1210
0
140
誤解だらけの開発生産性 / Myths and Misconceptions about Developer Productivity
i35_267
1
240
型は壁、Rustでもバグを直すな、表現できなくせよ
nwiizo
13
2k
Featured
See All Featured
Designing for humans not robots
tammielis
254
26k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
1.5k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.4k
Facilitating Awesome Meetings
lara
57
7k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
260
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
260
Writing Fast Ruby
sferik
630
63k
Accessibility Awareness
sabderemane
1
150
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.2k
Become a Pro
speakerdeck
PRO
31
6k
How STYLIGHT went responsive
nonsquared
100
6.2k
Transcript
The Next Generation of Testing in __
Turbo87 TobiasBieniek
simplabs based in Munich consulting all over ! * Stickers
available after the talk
Ember CLI
⏰ andThen() The andThen helper will wait for all preceding
asynchronous helpers to complete prior to progressing forward.
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')); });
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
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
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
None
30 kB no longer needed by Ember.js itself
30 kB still needed by the test helpers
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
jQuery await click('button.submit'); assert.equal(find('ul.posts li').text(), 'My new post');
jQuery import { click, find } from 'ember-native-dom-helpers'; await click('button.submit');
assert.equal(find('ul.posts li').textContent, 'My new post');
jQuery import { click, find } from '@ember/test-helpers'; await click('button.submit');
assert.equal(find('ul.posts li').textContent, 'My new post');
Nested Modules
Nested Modules describe('Galaxy', function() { describe('Earth', function() { describe('Portland', function()
{ it('is awesome at EmberConf', function() { // right? });aaa }); }); });
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 }); }); });
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 }); }); });
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()
The Grand Testing Unification
RFC #119
RFC #119
RFC #232
RFC #232 Replaces • moduleFor() • moduleForComponent() • moduleForModel() with
• module() + setupTest()
RFC #268
RFC #268 Replaces • moduleForAcceptance() with • module() + setupApplicationTest()
Plain QUnit tests for code unrelated to Ember
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
"Container" tests for Controllers, Routes, Services, ... (previously Unit /
Integration)
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
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
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
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()
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()
Rendering tests for Components and Helpers (previously Component-Integration)
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
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()
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()
Application tests to verify user stories (previously Acceptance)
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
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()
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()
ember install ???
ember install ember-cli-qunit 4.2.0+
Migration What about the 5000 tests we already have?
Migration async/await + ember-native-dom-helpers , ember-native-dom-helpers-codemod
ember-qunit-codemod Migration setupTest() functions .
ember-test-helpers-codemod Migration @ember/test-helpers /
Tips & Tricks ✨ ✨
Mocking aka. taking advantage of inject()
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
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
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']); });
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']); });
Loading States How can we test them?
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
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"
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
Loading States await waitFor('.loading-spinner'); same as: await waitUntil(() => find('.loading-spinner'));
Custom Test Helpers aka. registerAsyncHelper()
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'); }
Test Selectors the thing you know from CSS that helps
you find elements in the DOM
Test Selectors <h1>{{title}} </h1> <input class="title-field"> await fillIn('.title-field', 'Hello World!');
let title = this.element.querySelector('h1').textContent; assert.equal(title, 'Hello World!'); Test Selectors
Test Selectors <h1 data-test-title>{{title}} </h1> <input data-test-title-field> await fillIn('[data-test-title-field]', 'Hello
World!'); let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title, 'Hello World!');
Test Selectors <h1 data-test-title>{{title}} </h1> <input data-test-title-field> 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
Readable Assertions How to write code that is easy to
understand
Readable Assertions let title = this.element.querySelector('[data-test-title]').textContent; assert.equal(title.trim(), 'Hello World!');
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!');
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');
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
Thanks