Posted 3 months ago. Visible to the public. Repeats.

Jasmine: using async/await to write nice asynchronous specs

Jasmine has long standing support for writing asynchronous specs. In days gone by we used the done callback to achieve this, but these days it is possible to write much more readable specs.

Async specs

As a first example, say we want to check that some form disables the submit button while working.

Copy
// bad (how we used to do it) beforeEach(() => { this.form = setupMyForm() this.submitButton = findTheSubmitButton() }) it('disables the submit button while working', (done) => { expect(this.submitButton.disabled).toBe(false) let submitPromise = this.form.onSubmit() // assuming we exposed .onSubmit in our code and return a promise expect(this.submitButton.disabled).toBe(true) submitPromise.then(() => { // form is done expect(this.submitButton.disabled).toBe(false) done() // notify Jasmine we're finished }) })

Newer versions of Jasmine allow our specs to simply return a Promise, so we could rewrite this as

Copy
// slightly better it('disables the submit button while working', () => { expect(this.submitButton.disabled).toBe(false) let submitPromise = this.form.onSubmit() expect(this.submitButton.disabled).toBe(true) return submitPromise.then(() => { // form is done expect(this.submitButton.disabled).toBe(false) }) })

This becomes much more readable, when using the async/await syntax:

Copy
// good it('disables the submit button while working', async () => { expect(this.submitButton.disabled).toBe(false) let submitPromise = this.form.onSubmit() expect(this.submitButton.disabled).toBe(true) await submitPromise // form is done expect(this.submitButton.disabled).toBe(false) })

Async setup

Say setting up our form is also asynchronous and takes some time. To allow our specs to wait till we're done, we have exposed a property ourForm.ready which returns a promise that resolves when setup is finished.

Now we'll simply use an async function inside our beforeEach as well:

Copy
beforeEach(async () => { this.form = setupMyForm() await this.form.ready this.submitButton = findTheSubmitButton() })

Dealing with non-cooperative code

Sometimes, we want to await something asyncronous, but cannot get hold of a corresponding promise. In these cases, we can sometimes get away with simply waiting for a tick, or a set amount of time.

Copy
function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } it('handles an event', async () => { triggerAnEventOn(this.form) await wait(0) # one tick // now expect the desired result })

We can also wait a bit for a certain condition to occur:

Copy
async function waitFor(callback) { let msToWait = [0, 1, 5, 10, 100] // we use some exponential fall-off while (true) { let [msToWaitNow, ...msToWaitNext] = msToWait msToWait = msToWaitNext if (msToWaitNow != undefined) { await wait(msToWaitNow) let result = callback() if (result) { return result } } else { throw("timed out waiting") } } } it('made a request', async () => { triggerAnEventOn(this.form) let request = await waitFor(() => { jasmine.Ajax.requests.mostRecent() }) // now expect the request to be correct })

Asynchronous spies

If you want to mock an asychronous method, the following riff on Jasmine's spyOn method might be useful:

Copy
function asyncSpyOn(object, methodName) { let spy = spyOn(object, methodName) spy.and.returnValue(new Promise((resolve, reject) => { spy.resolve = resolve spy.reject = reject })) return spy }

You can use it to mock an asynchronous method, check it has been called, and then resolve it inside your spec.

Copy
it('generates some token', async () => { let spy = asyncSpyOn(this.form, 'createToken') this.form.emailField.value = 'user@example.org' // assuming we exposed emailField let submitPromise = this.form.onSubmit() expect(spy).toHaveBeenCalledWith('user@example.org') spy.resolve('the token') await submitPromise // now expect the form to have used the token })

Alternatively, you can resolve the spy preemptively:

Copy
it('generates some token', async () => { asyncSpyOn(this.form, 'createToken').resolve('the token') await this.form.onSubmit() // now expect the form to have used the token })

Once an application no longer requires constant development, it needs periodic maintenance for stable and secure operation. makandra offers monthly maintenance contracts that let you focus on your business while we make sure the lights stay on.

Owner of this card:

Avatar
Tobias Kraze
Last edit:
3 months ago
by Besprechungs-PC
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Tobias Kraze to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more