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:
CopybeforeEach(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.
Copyfunction 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:
Copyasync 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:
Copyfunction 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.
Copyit('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:
Copyit('generates some token', async () => { asyncSpyOn(this.form, 'createToken').resolve('the token') await this.form.onSubmit() // now expect the form to have used the token })
By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.