Deterministic testing of a polling task in EmberJS
Michal Bryxí
Posted on January 21, 2020
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!');
});
});
Posted on January 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.