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 loads product reviews when it's created and provides an ability to save a single review using vue-resource.

It's very bare bones, but hopefully will serve well enough as a minimal example:

export default {
  name: 'ProductReviews',
  props: {
    getReviewsEndpoint: [
      type: String,
      default: '/reviews'
    ],
    postReviewEndpoint: [
      type: String,
      default: '/review'
    ]
  },
  data () {
    return {
      review: '',
      reviews: []
    }
  },
  methods: {
    loadReviews () {
      this.$http.get(this.getReviewsEndpoint).then(response => {
        this.reviews = response.body
      }, response => {
        // handle error
      })
    },
    saveReview () {
      this.$http.post(this.postReviewEndpoint, {
        review: this.review
      }).then(response => {
        this.reviews.push(this.review)
      }, response => {
        // handle error
      })
    }
  },
  created () {
    this.loadReviews()
  }
}

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 flushPromises from 'flush-promises'
import ProductReviews from './ProductReviews'
  
it('loads reviews', () => {
  const productReviews = shallow(ProductReviews, {
    mocks: {
      $http: {
        get: () => new Promise(resolve => resolve({ body: ['review A', 'review B'] }))
      }
    }
  })
  
  flushPromises().then(() => {
    expect(productReviews.vm.reviews).to.deep.equal(['review A', 'review B'])
  })
})

You may be wondering what flushPromises does. It basically flashes all pending resolved promise handlers, so that it's safe to make assertions about the component state that is a result of a successful API call.

If you take a look under the hood (https://github.com/kentor/flush-promises) the function is just a few lines of code.

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

it('saves a review', () => {
  const productReviews = shallow(ProductReviews, {
    mocks: {
      $http: {
        get: () => new Promise(resolve => resolve({ body: [] })),
        post: () => new Promise.resolve()
      }
    }
  })
  
  productReviews.vm.review = 'Keep calm and mock API calls.'
  productReviews.vm.saveReview()
  
  flushPromises().then(() => {
    expect(productReviews.vm.reviews).to.deep.equal(['Keep calm and mock API calls.'])
  })
})

Just in case you need to mock different API calls using the same HTTP method here's an idea:

propsData: {
  getReviewsEndpoint: '/reviews',
  getCommentsEndpoint: '/comments'
},
mocks: {
  $http: {
    get: (url, config) => {
      switch (url) {
        case '/reviews':
          return new Promise(resolve => resolve({ body: ['review A', 'review B'] }))
        case '/comments':
          return new Promise(resolve => resolve({ body: ['comment A', 'comment B'] }))
        default:
          return new Promise.reject()
      }
    }
  }
}
Did you like this post?
Previous post

Portals provide a way to render content into a DOM node that exists outside the hierarchy of the Vue component that creates the portal. The concept of portals also exists in React. When the need arises I rely on the portal-vue library to create portals, which provides a really clean syntax.

Next post

Expo comes with an Audio API that provides basic sound playback and recording capabilities in React Native projects. It includes a lot of useful features, but at times can be tricky to work with because of its asynchronous nature. I collected a few bits of information that can help with setting up and playing audio in Expo. 🎛️