loading...

Deterministic testing of a polling task in EmberJS

michalbryxi profile image Michal Bryxí ・2 min read

The problem

Let's have a service with following ember-concurrency tasks. The service will do some work in an infinite loop. Let's say poll data from a backend:

// services/data-fetch.js

export default Service.extend({
  startPolling: task(function* () {
    yield timeout(1000);
    this.fetchData.perform();
  }),

  fetchData: task(function* () {
    let updatedData = yield backend.fetchData;
    this.set('data', updatedData);

    this.startPolling.perform();
  }),
});

We can then start the polling by simply clicking on a button:

// my-component.hbs

<button {{on "click" (perform dataFetch.startPolling)}}>
  start
</button>

<p data-test-current-value>
  Current value is: {{dataFetch.data}}
</p>

Let's now say our backend will give us a sequence of following values: ["preparing task", "preparing task", "processing task", "saving results", "done", "done", "done", ...]

And our task (got it?) is to check that the UI printed out at one point the string processing task. How do we do it? How do we await in tests till after certain amount of iterations or after certain amount of time?

Note about active waiting in tests

You can use waitUntil from ember-test-helpers:

// my-component-test.js

await this.component.clickButton();
await waitUntil(() => this.component.currentValue === 'processing task');
assert.equal(this.component.currentValue, 'processing task', 'yay!');

The thing is: If you can avoid active waiting (yield timeout(1000)) in tests, you really should. Ember tests are in general very quick and few of those waiters can significantly slow down your test suite.

Also the problem here is that waitUntil polls for changes in regular intervals, so if it lags for any reason, then you might miss your desired state and end on one of the states after that. And trust me that this situation happens more than you would expect (it's the reason for this article).

Solution

The idea is to bypass the automatic polling and make it more deterministic by driving the requests manually.

// my-component-test.js
module('Integration | Component | polling button', function(hooks) {
  setupRenderingTest(hooks);

  hooks.beforeEach(function() {
    // ember-cli-page-object helper
    this.component = create({
      clickButton: clickable("button"),
      currentValue: text("[data-test-current-value]")
    });

    this.dataFetch = this.owner.lookup('service:data-fetch');
    // This slightly weird syntax is needed because we're stubbing ember-concurrency task
    sinon.stub(this.dataFetch.startPolling, 'perform');
  });

  test('it can display "processing task"', async function(assert) {
    await render(hbs`{{my-component}}`);

    await this.component.clickButton();

    // Manually advance fetch-data service internal state
    await this.dataFetch.fetchData.perform();
    assert.equal(this.component.currentValue, 'preparing task', 'we are preparing task');

    await this.dataFetch.fetchData.perform();
    await this.dataFetch.fetchData.perform();
    assert.equal(this.component.currentValue, 'processing task', 'yay!');
  });
});

Discussion

pic
Editor guide
Collapse
profikid profile image
Laurens Profittlich

Nice article!