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

EmberFest 2017: Testing against Time - Meaningf...

EmberFest 2017: Testing against Time - Meaningful testing in Ember apps when timing matters

Ember offers a rich API and a wide set of helpers and blueprints to make testing in your apps fast and straight-forward. But as the applications we write tests against grow more complex, we might find ourselves stumbling into test timeouts, eventually succeeding tests and other hard-to-reason about test errors brought on by asynchronous and time-dependent behaviour.

This talk will give an insight into how to create meaningful test cases for async or other, time-related operations in our applications. We will see how we can leverage Ember's test helpers, newest JavaScript features and Ember community addons to reduce non-determinism in our test suites and make those tests turn green even as time passes by.

Jessy Jordan

October 13, 2017
Tweet

More Decks by Jessy Jordan

Other Decks in Programming

Transcript

  1. TESTING: HOW AND WHY WHY WE DO TESTING WORK WITH

    COOL AND FANCY TESTING FRAMEWORKS ✨ PUSHING BACK RELEASE DATES SPEND MORE TIME AT WORK
  2. TESTING: HOW AND WHY WHY WE DO TESTING SHIP CONFIDENTLY

    PREVENT SUBSEQUENT BUGS INTERNAL DOCUMENTATION
  3. WAITING FOR ASYNCHRONOUS OPERATIONS TESTING ROUTE TRANSITIONS export default Route.extend({

    model() { const store = this.get('store'); const repoFetches = this.get('reposList').map((repo) => { return store.findRecord('github-org', repo); }); return all(repoFetches); }, });
  4. WAITING FOR ASYNCHRONOUS OPERATIONS EMBER ASYNC HELPERS: VISIT export default

    function visit(app, url) { let router = app.__container__.lookup('router:main'); app.boot().then(() => { router.location.setURL(url); // ... }); // teardown work return app.testHelpers.wait(); }
  5. WAITING FOR ASYNCHRONOUS OPERATIONS import { test } from 'qunit';

    import moduleForAcceptance from 'whats-new-in-emberland/tests/helpers/ module-for-acceptance'; moduleForAcceptance('Acceptance | overview'); test('visiting /overview', function(assert) { visit('overview'); andThen(function() { assert.equal(currentURL(), '/overview'); }); });
  6. WAITING FOR ASYNCHRONOUS OPERATIONS visit() click() fillIn() keyEvent() triggerEvent() export

    default function visit(app, url){ // … return app.testHelpers.wait(); } BUILT-IN ASYNCHRONOUS HELPERS FOR ACCEPTANCE TESTING
  7. WAITING FOR ASYNCHRONOUS OPERATIONS TESTING ASYNC USER INTERACTIONS import Component

    from '@ember/component'; import { debounce } from '@ember/runloop'; export default Component.extend({ pull: null, actions: { loadComments(pull) { this.set('isLoadingComments', true); debounce(this, this.loadComments, pull, 800); }, }, });
  8. WAITING FOR ASYNCHRONOUS OPERATIONS TESTING ASYNC USER INTERACTIONS import Component

    from '@ember/component'; import { debounce } from '@ember/runloop'; export default Component.extend({ pull: null, comments: null, loadComments(pull) { const commentsUrl = pull.get('commentsUrl'); return this.get(‘request’).fetch(commentsUrl).then((comments) => { this.set('comments', comments); this.set('isLoadingComments', false); }); }, actions: { loadComments(pull) { this.set('isLoadingComments', true); debounce(this, this.loadComments, pull, 800); }, }, });
  9. WAITING FOR ASYNCHRONOUS OPERATIONS moduleForComponent('news-item', 'Integration | Component | news

    item'); test(‘loading comments via user interaction', function(assert) { // ember install ember-data-factory-guy this.set('pull', make('github-pull')); this.render(hbs`{{news-item pull=pull repo=repo}}`); $(‘[data-test-load-comments]').click(); assert.equal($('[data-test-num-of-comments]').text(), `2`, 'displays comments'); });
  10. WAITING FOR ASYNCHRONOUS OPERATIONS moduleForComponent(‘news-item', 'Integration | Component | news

    item', { beforeEach() { this.register('service:request', Service.extend({ fetch: td.function(), }); }, });
  11. WAITING FOR ASYNCHRONOUS OPERATIONS moduleForComponent('news-item', 'Integration | Component | news

    item'); test('loading comments via user interaction', function(assert) { const pull = make(‘github-pull') this.set('pull', pull); this.set('comments', makeList('github-comment', 2)); const commentsUrl = 'https://api.github.com/repos/user1/repository/pulls/1/comments'; td.when(this.get(‘request’).fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`); $(‘[data-test-load-comments]').click(); assert.equal($(‘[data-test-num-of-comments]’).text().trim(), `2`, 'displays comments'); });
  12. WAITING FOR ASYNCHRONOUS OPERATIONS const fetch = td.function(); td.when(this.get(‘request’) .fetch(commentsUrl))

    .thenResolve(this.comments); loadComments(pull) { const commentsUrl = pull.get('commentsUrl'); return this.get(‘request’) .fetch(commentsUrl) .then((comments) => { this.set('comments', comments); this.set('isLoadingComments', false); }); }, app/components/news-item.js tests/integration/components/news-item-test.js
  13. WAITING FOR ASYNCHRONOUS OPERATIONS moduleForComponent('news-item', 'Integration | Component | news

    item'); test('loading comments via user interaction', function(assert) { const pull = make(‘github-pull') this.set('pull', pull); this.set('comments', makeList('github-comment', 2)); const commentsUrl = 'https://api.github.com/repos/user1/repository/pulls/1/comments'; td.when(this.get(‘request’) .fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`); $(‘[data-test-load-comments]’).click(); assert.equal($('[data-test-num-of-comments]').text(), `2`, 'displays comments'); });
  14. WAITING FOR ASYNCHRONOUS OPERATIONS import { wait } from 'ember-test-helpers/wait';

    moduleForComponent('news-item', 'Integration | Component | news item'); test('loading comments via user interaction', function(assert) { const pull = make(‘github-pull') this.set('pull', pull); this.set('comments', makeList('github-comment', 2)); const commentsUrl = 'https://api.github.com/repos/user1/repository/pulls/1/comments'; td.when(this.get(‘request’).fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`); $('[data-test-load-comments]').click(); return wait(() => { assert.equal($('[data-test-num-of-comments]').text(), `2`, 'displays comments'); }); });
  15. WAITING FOR ASYNCHRONOUS OPERATIONS import { wait } from 'ember-test-helpers/wait';

    moduleForComponent('news-item', 'Integration | Component | news item'); test('loading comments via user interaction', function(assert) { const pull = make(‘github-pull') this.set('pull', pull); this.set('comments', makeList('github-comment', 2)); const commentsUrl = 'https://api.github.com/repos/user1/repository/pulls/1/comments'; td.when(this.get(‘request’).fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`); $('[data-test-load-comments]').click(); return wait(() => { assert.equal($('[data-test-num-of-comments]').text(), `2`, 'displays comments'); }); });
  16. WAITING FOR ASYNCHRONOUS OPERATIONS await fillIn() await click() await keyEvent()

    await triggerEvent() await focus() await blur() await tap() EMBER-NATIVE-DOM-HELPERS & AWAIT / ASYNC SUITABLE FOR BOTH YOUR ACCEPTANCE & INTEGRATION TESTS
  17. WAITING FOR ASYNCHRONOUS OPERATIONS import { click } from 'ember-native-dom-helpers';

    moduleForComponent('news-item', 'Integration | Component | news item'); test('loading comments via user interaction', async function(assert) { this.set('pull', make('github-pull')); this.set('comments', makeList('github-comment', 2)); const commentsUrl = 'https://api.github.com/repos/user1/repository/pulls/1/comments'; td.when(this.get(‘request’).fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`); await click(‘[data-test-load-comments]’); assert.equal($('[data-test-num-of-comments]').text(), `2`, 'displays comments'); });
  18. WAITING FOR ASYNCHRONOUS OPERATIONS export default Component.extend({ //… reloadComments: task(function

    * () { const commentsUrl = this.get('pull.commentsUrl'); const comments = yield this.get(‘request’) .fetch(this.get(‘pull.commentsUrl’)); this.set('comments', comments); }).drop(), });
  19. WAITING FOR ASYNCHRONOUS OPERATIONS export default Component.extend({ startReloading: task(function *(){

    while(true) { this.get(‘reloadComments').perform(); yield timeout(50000); }, reloadComments: task(function * () { const commentsUrl = this.get('pull.commentsUrl'); const comments = yield this.get(‘request’) .fetch(this.get(‘pull.commentsUrl’)); this.set('comments', comments); }).drop(), });
  20. WAITING FOR ASYNCHRONOUS OPERATIONS td.when(this.get(‘request’) .fetch(commentsUrl)) .thenResolve(this.comments); this.render(hbs`{{news-item pull=pull repo=repo}}`);

    return wait().then(() => { assert.equal(find('[data-test-num-of-comments]').textContent.trim(), `2`, 'displays comments'); });
  21. WAITING FOR ASYNCHRONOUS OPERATIONS this.render(hbs`{{news-item pull=pull repo=repo}}`); later(() => {

    run.cancelTimers(); }, 500); return wait().then(() => { assert.equal(find('[data-test-num-of-comments]').textContent.trim(), `2`, 'displays comments'); });
  22. WAITING FOR ASYNCHRONOUS OPERATIONS this.render(hbs`{{news-item pull=pull repo=repo}}`); later(() => {

    run.cancelTimers(); }, 500000); return wait().then(() => { assert.equal(find('[data-test-num-of-comments]').textContent.trim(), `2`, 'displays comments'); });
  23. WAITING FOR ASYNCHRONOUS OPERATIONS const TIMEOUT_INTERVAL = Ember.testing ? 1

    : 500000; //… startReloading: task(function *(){ while(true) { yield timeout(TIMEOUT_INTERVAL); this.get(‘reloadComments').perform(); } }
  24. this.render(hbs`{{news-item pull=pull repo=repo}}`); later(() => { run.cancelTimers(); }, 50); return

    wait().then(() => { assert.equal(find('[data-test-num-of-comments]').textContent.trim(), `2`, 'displays comments'); });
  25. WAITING FOR ASYNCHRONOUS OPERATIONS FURTHER READING Ember Concurrency Docs on

    Testing: https://ember-concurrency.com/#/docs/testing-debugging Ember Testing Unificationn RFC: https://github.com/emberjs/rfcs/pull/119
  26. WAITING FOR ASYNCHRONOUS OPERATIONS test('the overview page doesn\'t stress me

    out with release date disclaimers', function(assert) { visit('/overview'); andThen(function() { assert.notOk(find('[data-test-is-thursday-disclaimer]'), 'doesn\'t display Thu disclaimer'); assert.notOk(find('[data-test-is-friday-disclaimer]'), 'doesn\'t display Fri disclaimer'); }); });
  27. WAITING FOR ASYNCHRONOUS OPERATIONS test('the overview page doesn\'t stress me

    out with release date disclaimers', function(assert) { visit('/overview'); andThen(function() { assert.notOk(find('[data-test-is-thursday-disclaimer]'), 'doesn\'t display Thu disclaimer'); assert.notOk(find('[data-test-is-friday-disclaimer]'), 'doesn\'t display Fri disclaimer'); }); }); WHEN THIS TEST IS RUN ON A THURSDAY OR FRIDAY NOT OK
  28. WAITING FOR ASYNCHRONOUS OPERATIONS moduleForAcceptance('Acceptance | overview', { beforeEach() Timecop.install();

    Timecop.travel(new Date(2017, 9, 11, 11, 45)); }, afterEach(){ Timecop.uninstall(); } }); EMBER INSTALL EMBER-CLI-TIMECOP
  29. WAITING FOR ASYNCHRONOUS OPERATIONS test('the overview page doesn\'t stress me

    out with release date disclaimers', function(assert) { visit('/overview'); andThen(function() { assert.notOk(find('[data-test-is-thursday-disclaimer]'), 'doesn\'t display Thursday disclaimer'); assert.notOk(find('[data-test-is-friday-disclaimer]'), 'doesn\'t display Friday disclaimer'); }); }); moduleForAcceptance('Acceptance | overview', { beforeEach() Timecop.install(); Timecop.travel(new Date(2017, 9, 11, 11, 45)); // is a Wednesday: 11.10.2017 }, afterEach(){ Timecop.uninstall(); } });
  30. WAITING FOR ASYNCHRONOUS OPERATIONS test('the overview page…oh no, it’s Friday’,

    function(assert) { Timecop.travel(new Date(2017, 9, 13, 16, 20)); // is today’s Friday! :o visit('/overview'); andThen(function() { assert.notOk(find('[data-test-is-thursday-disclaimer]'), 'doesn\'t display Thursday disclaimer'); assert.ok(find('[data-test-is-friday-disclaimer]'), ‘the newsletter has to get out - oh no!’); }); });