Mock asynchronous API calls in Vue component tests

This post collects a few notes on how to mock asynchronous API calls when testing Vue components.

Let's start with a Vue component that sends a GET request to the back-end and loads product reviews using vue-resource. It's very bare bones, but hopefully will serve well enough as a minimal example:

export default {
  name: 'ProductReviews',
  data () {
    return {
      reviews: []
    }
  },
  methods: {
    loadReviews () {
      this.$http.get('/reviews').then(response => {
        this.reviews = response.body
      }, response => {
        // handle error
      })
    }
  }
}

Vue-resource (similarly to axios and many other HTTP clients) is Promise based. What it means is that every API call made with vue-resource returns a Promise, which is resolved when the API call was successful and rejected otherwise.

Knowing that, methods that call the API can be mocked and return a Promise instead of the actual response:

import { shallow } from 'vue-test-utils'
import { expect } from 'chai'
import ProductReviews from './ProductReviews'
  
it('loads reviews', () => {
  const response = Promise.resolve({ body: ['review A', 'review B'] })
  const productReviews = shallow(ProductReviews, {
    mocks: {
      $http: {
        get: () => response
      }
    }
  })

  productReviews.vm.loadReviews()
  
  return response.then(() => {
    expect(productReviews.vm.reviews).to.deep.equal(['review A', 'review B'])
  })
})

Quite often when testing asynchronous code it's necessary to call a done() callback to let the test runner know the test case has completed. In this example I'm using Mocha, which is supporting Promise return values. Returning a Promise ensures that the assertions are met before the test runner continues to the next test case.

In a similar way we can now test a method that saves a new review:

export default {
  name: 'ProductReview',
  data () {
    return {
      saved: false,
      review: ''
    }
  },
  methods: {
    saveReview () {
      this.$http.post('/review', { review: this.review })
        .then(response => {
          this.saved = true
        }, response => {
          // handle error
        })
    }
  }
}

This time we'll also spy on the POST request data:

import { shallow } from 'vue-test-utils'
import { expect } from 'chai'
import sinon from 'sinon'
import ProductReview from './ProductReview'

it('saves a review', () => {
  const spy = sinon.spy()
  const response = new Promise.resolve()
  const productReview = shallow(ProductReview, {
    mocks: {
      $http: {
        post: (url, data) => {
          spy(data.review)
          return response
        }
      }
    }
  })
  
  productReview.vm.review = 'Keep calm and mock API calls.'
  productReview.vm.saveReview()
  
  return response.then(() => {
    expect(spy.withArgs('Keep calm and mock API calls.').calledOnce).to.be.true    
    expect(productReview.vm.saved).to.be.true
  })
})

Not always everything goes according to the plan - to test failed API requests the Promise should reject:

const failedResponse = Promise.reject({ error: 'Something went wrong' })

Sometimes you may be forced to mock multiple API requests made in the same component. This can be achieved by using Promise.all():

return Promise.all([getResponse, postResponse]).then(() => {
  // assertions
})

I know of at least one more way to mock API requests when using vue-resource and that is by doing it with HTTP request interceptors. Mathias Hager wrote a very good article on the subject:

https://matthiashager.com/mocking-http-requests-with-vuejs