Slide 1

Slide 1 text

Testing against time in JavaScript Applications Assert (js) 2018 @ J J O R D A N _ D E V J E S S I C A J O R D A N

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

JavaScript Apps are full of asynchronous behaviour Events & User Interaction Timeouts Ajax Requests …and many more

Slide 5

Slide 5 text

Testing that can be even harder

Slide 6

Slide 6 text

If we get complacent though…

Slide 7

Slide 7 text

…so let’s find out how to do it.

Slide 8

Slide 8 text

Sidetrack Testing in Ember Apps 101

Slide 9

Slide 9 text

+ +

Slide 10

Slide 10 text

import { module, test } from 'qunit'; module('some-thing', function(hooks) { test('it computes foo', function(assert) { }); });

Slide 11

Slide 11 text

import { module, test } from 'qunit'; module('some-thing', function(hooks) { hooks.beforeEach(function(){ // custom setup here }); hooks.afterEach(function(){ // custom teardown here }); // test(‘test1', …); // test(‘test2’, …); // test(‘test3’,…); });

Slide 12

Slide 12 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('some-thing', function(hooks) { setupTest(hooks); test('it computes computedFoo', async function(assert) { }); });

Slide 13

Slide 13 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('some-thing', function(hooks) { setupTest(hooks); test('it computes computedFoo', async function(assert) { const someThing = this.owner.lookup('service:some-thing'); }); });

Slide 14

Slide 14 text

import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('some-thing', function(hooks) { setupTest(hooks); test('it computes computedFoo', async function(assert) { const someThing = this.owner.lookup('service:some-thing'); someThing.set('foo', 'baz'); assert.equal(someThing.get('computedFoo'), 'computed baz'); }); });

Slide 15

Slide 15 text

Making Tests Wait
 Async Ops

Slide 16

Slide 16 text

Why is it async? Routing Loading data for a view Handling route transitions Provide context for a view

Slide 17

Slide 17 text

Why is it async? Routing Loading data for a view Handling route transitions Provide context for a view

Slide 18

Slide 18 text

ROUTING Transition States

Slide 19

Slide 19 text

return $.getJSON('https://api.github.com/repos/emberjs/website/pulls') .then((response) => { return response; });

Slide 20

Slide 20 text

Ember Data Github Addon Documentation: https://github.com/elwayman02/ember-data-github#ember-data-github Loading Records in Ember Applications via Ember-Data: https://guides.emberjs.com/v3.0.0/routing/specifying-a-routes-model/ export default Route.extend({ model() { const store = this.get('store'); return store.query('github-pull', { repo: 'emberjs/website' }); }, });

Slide 21

Slide 21 text

test('the main page displays a list of PRs', function(assert) { ourOwnVisitHelper('main'); });

Slide 22

Slide 22 text

High Level DOM Assertions With Qunit Dom: https://github.com/simplabs/qunit-dom DOM Assertions with QUnit-DOM test('the main page displays a list of PRs', function(assert) { ourOwnVisitHelper('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 23

Slide 23 text

❌ test('the main page displays a list of PRs', function(assert) { ourOwnVisitHelper('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 24

Slide 24 text

test('the main page displays a list of PRs', function(assert) { ourOwnVisitHelper('main'); await timeout(300); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 25

Slide 25 text

❌ ✅ test('the main page displays a list of PRs', async function(assert) { ourOwnVisitHelper('main'); await timeout(300); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 26

Slide 26 text

✅ but… test('the main page displays a list of PRs', async function(assert) { ourOwnVisitHelper('main'); await timeout(300000000000000000000000000000000000000000); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 27

Slide 27 text

…these tests become slow….

Slide 28

Slide 28 text

@ember/test-helpers Framework Agnostic Test Helpers for Ember Apps: https://github.com/emberjs/ember-test-helpers/blob/master/API.md

Slide 29

Slide 29 text

High Level DOM Assertions With Qunit Dom: https://github.com/simplabs/qunit-dom test('the main page displays a list of PRs', function(assert) { ourOwnVisitHelper('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 30

Slide 30 text

import { visit } from '@ember/test-helpers'; // ... test('the main page displays a list of PRs’, function(assert) { visit('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 31

Slide 31 text

import { visit } from '@ember/test-helpers'; // ... test('the main page displays a list of PRs', async function(assert) { await visit('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 32

Slide 32 text

✅ import { visit } from '@ember/test-helpers'; // ... test('the main page displays a list of PRs', async function(assert) { await visit('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });

Slide 33

Slide 33 text

…will be waiting for async behaviour to settle Helpers for DOM Interaction & more… visit() click() fillIn() keyEvent() triggerEvent() … export default function visit(url){ //... return settled(); }

Slide 34

Slide 34 text

se ttl ed function settled( ) { } hasRunLoop? hasPendingTime rs? hasPendingWait ers? hasPendingReq uests?

Slide 35

Slide 35 text

function settled( ) { } hasRunLoop? hasPendingTime rs? hasPendingWait ers? hasPendingReq uests? every 10 ms se ttl ed

Slide 36

Slide 36 text

function settled( ) { } hasRunLoop? hasPendingTime rs? hasPendingWait ers? hasPendingReq uests? se ttl ed ✅ ✅ ✅ ✅ resolved Promise

Slide 37

Slide 37 text

Writing your own waiting helper See also waitUntil helper from @ember/test-helpers function myOwnWaitHelper(callback) { return new Promise(function(resolve, reject) { let time = 0; // you could of course also have a timer that tracks when this should time out and just reject function checkAssertion(currentTime) { setTImeout(function() { let value = callback(); time += 10; if (value) { // resolve if assertion is correct resolve(value); } else if (timerIsNotUpYet) { checkAssertion(time); } else { reject(value); } }, 10); } checkAssertion(0); }); }

Slide 38

Slide 38 text

Writing your own waiting helper See also waitUntil helper from @ember/test-helpers function myOwnWaitHelper(callback) { return new Promise(function(resolve, reject) { let time = 0; // you could of course also have a timer that tracks when this should time out and just reject function checkAssertion(currentTime) { setTImeout(function() { let value = callback(); time += 10; if (value) { // resolve if assertion is correct resolve(value); } else if (timerIsNotUpYet) { checkAssertion(time); } else { reject(value); } }, 10); } checkAssertion(0); }); }

Slide 39

Slide 39 text

Writing your own waiting helper See also waitUntil helper from @ember/test-helpers function myOwnWaitHelper(callback) { return new Promise(function(resolve, reject) { let time = 0; // you could of course also have a timer that tracks when this should time out and just reject function checkAssertion(currentTime) { setTImeout(function() { let value = callback(); time += 10; if (value) { // resolve if assertion is correct resolve(value); } else if (timerIsNotUpYet) { checkAssertion(time); } else { reject(value); } }, 10); } checkAssertion(0); }); }

Slide 40

Slide 40 text

✨Testing Magic ✨

Slide 41

Slide 41 text

Async? User Interacti on events might trigger other async operations

Slide 42

Slide 42 text

app/components/pull-request-item.js loadComments(pull) { const commentsUrl = pull.get('commentsUrl'); return this.get('request') .fetch(commentsUrl) .then((comments) => { this.set('comments', comments); }); },

Slide 43

Slide 43 text

test('loading comments', async function(assert) { await render(hbs`{{pull-request-item pullRequest=pullRequest}}`); await click('button.load-comments'); assert.dom(‘.list-item .comment').hasText('Comment 1: LGTM!'); });

Slide 44

Slide 44 text

Mocking Requests td.when().thenResolve() td.when().thenReject() td.when().thenCallback()

Slide 45

Slide 45 text

this.owner.register(‘service:request', RequestService.extend({ fetch: td.function(), });

Slide 46

Slide 46 text

test('loading comments', async function(assert) { this.owner.register(‘service:request', RequestService.extend({ fetch: td.function(), }); td.when(this.get('request').fetch(commentsUrl)) .thenResolve(this.mockComments); await render(hbs`{{pull-request-item pullRequest=pullRequest}}`); await click('button.load-comments'); assert.dom('.list-item .comment').hasText('Comment 1: LGTM!'); });

Slide 47

Slide 47 text

✅ test('loading comments', async function(assert) { this.owner.register(‘service:request', RequestService.extend({ fetch: td.function(), }); td.when(this.get('request').fetch(commentsUrl)) .thenResolve(this.mockComments); await render(hbs`{{pull-request-item pullRequest=pullRequest}}`); await click('button.load-comments'); assert.dom('.list-item .comment').hasText('Comment 1: LGTM!'); });

Slide 48

Slide 48 text

Making tests pass anytime 
 Time Travel

Slide 49

Slide 49 text

test('shows the week day', async function(assert) { await visit('main'); assert.dom('aside.week-day').hasText('Monday'); });

Slide 50

Slide 50 text

works when run on Mondays… ✅ test('shows the week day', async function(assert) { await visit('main'); assert.dom('aside.week-day').hasText('Monday'); });

Slide 51

Slide 51 text

…and fails Tuesday - Sunday ❌ test('shows the week day', async function(assert) { await visit('main'); assert.dom('aside.week-day').hasText('Monday'); });

Slide 52

Slide 52 text

Timecop.travel(new Date(2017, 9, 11, 11, 45)); // is a Monday

Slide 53

Slide 53 text

hooks.beforeEach(function() Timecop.install(); Timecop.travel(new Date(2017, 9, 11, 11, 45)); // is a Monday }); hooks.afterEach(function(){ Timecop.uninstall(); });

Slide 54

Slide 54 text

✅ module('Acceptance | overview', function(hooks) { hooks.beforeEach() Timecop.install(); Timecop.travel(new Date(2017, 9, 11, 11, 45)); // is a Monday }, hooks.afterEach(){ Timecop.uninstall(); } test('shows the weekday', async function(assert) { await visit('main'); assert.dom('aside.week-day').hasText('Monday'); }); });

Slide 55

Slide 55 text

Timing in your tests matters

Slide 56

Slide 56 text

Thank you. @ J J O R D A N _ D E V GITHUB:JESSICA-JORDAN @Assert(js) 2018