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

BWS 2018: Testing Against Time in JavaScript Ap...

BWS 2018: Testing Against Time in JavaScript Applications

As the applications we write tests against grow more complex we might stumble into testing errors stemming from asynchronous and time-dependent behaviour. This talk will characterize several roadblocks in testing which are affected by time. With an example Ember.js application, we will explore how Ember's rich testing API, newest JavaScript features and community libraries, will help us writing legible and reliable tests and how we can make those tests turn green even as time passes by.

30min talk given at the Bulgaria Web Summit 2018.

Jessy Jordan

April 14, 2018
Tweet

More Decks by Jessy Jordan

Other Decks in Programming

Transcript

  1. Testing against time in JavaScript Applications BWS 2018 @ J

    J O R D A N _ D E V J E S S I C A J O R D A N
  2. JavaScript Apps are full of asynchronous behaviour Events & User

    Interaction Timeouts Ajax Requests …and many more
  3. + +

  4. import { module, test } from 'qunit'; module('some-thing', function(hooks) {

    test('it computes foo', function(assert) { }); });
  5. 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’,…); });
  6. import { module, test } from 'qunit'; import { setupTest

    } from 'ember-qunit'; module('some-thing', function(hooks) { setupTest(hooks); test('it computes computedFoo', async function(assert) { }); });
  7. 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'); }); });
  8. 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'); }); });
  9. Why is it async? Routing Loading data for a view

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

    Handling route transitions Provide context for a view
  11. 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' }); }, });
  12. test('the main page displays a list of PRs', function(assert) {

    ourOwnVisitHelper('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });
  13. 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'); }); DOM Assertions with QUnit Dom
  14. ❌ test('the main page displays a list of PRs', function(assert)

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

    ourOwnVisitHelper('main'); assert.dom('.list-item').hasText('WIP: Fix Tomster Logo'); });
  16. export default Route.extend({ model() { return $.getJSON( 'https://api.github.com/repos/emberjs/website/pulls' ).then((response) =>

    { return response; }); }, }); once model hook resolves, route transition can progress
  17. 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'); });
  18. ❌ ✅ 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'); });
  19. ✅ 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'); });
  20. 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'); });
  21. 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'); });
  22. 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'); });
  23. ✅ 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'); });
  24. …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(); }
  25. se ttl ed function settled( ) { } hasRunLoop? hasPendingTime

    rs? hasPendingWait ers? hasPendingReq uests?
  26. function settled( ) { } hasRunLoop? hasPendingTime rs? hasPendingWait ers?

    hasPendingReq uests? se ttl ed ✅ ✅ ✅ ✅ resolved Promise
  27. 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); }); }
  28. 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); }); }
  29. 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); }); }
  30. app/components/pull-request-item.js export default Component.extend({ request: inject(), // . . .

    loadComments(pull) { const commentsUrl = pull.get('commentsUrl'); return this.get('request') .fetch(commentsUrl) .then((comments) => { this.set('comments', comments); }); }, }); making use of a dedicated network Service
  31. 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!'); });
  32. ✅ 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!'); });
  33. works when run on Mondays… ✅ test('shows the week day',

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

    async function(assert) { await visit('main'); assert.dom('aside.week-day').hasText('Monday'); });
  35. hooks.beforeEach(function() Timecop.install(); Timecop.travel(new Date(2017, 9, 11, 11, 45)); // is

    a Monday }); hooks.afterEach(function(){ Timecop.uninstall(); });
  36. ✅ 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'); }); });
  37. Overcome timing challenges in tests Use time travel to test

    time-dependent code Mock async network requests reliably Wait for application state to settle Make tests pass regardless of execution order …
  38. Thank you. @ J J O R D A N

    _ D E V GITHUB:JESSICA-JORDAN @Bulgaria Web Summit 2018