Posted 19 days ago. Visible to the public. Linked content. Auto-destruct in 41 days

Updated: Jasmine: using async/await to write nice asynchronous specs

The await wait(ms) can now be used while time is mocked.

Changes

  • 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.
  • ```js
  • // 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
  • ```js
  • // 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](/makandra/46897):
  • ```js
  • // 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:
  • -```
  • +```js
  • 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.
  • -```
  • +```js
  • +// Use the original `setTimeout()` before it is mocked by `jasmine.clock.install()`.
  • +const unmockedTimeout = window.setTimeout
  • +
  • function wait(ms) {
  • return new Promise((resolve) => {
  • -setTimeout(resolve, ms)
  • +unmockedTimeout(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:
  • -```
  • +```js
  • 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:
  • ```js
  • 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.
  • ```js
  • 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:
  • ```js
  • 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
  • })
  • ```

Your development team has a full backlog of feature requests, chores and refactoring coupled with deadlines? We are familiar with that. With our "DevOps as a Service" offering, we support developer teams with infrastructure and operations expertise.

Owner of this card:

Avatar
Henning Koch
Last edit:
19 days ago
by Henning Koch
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more