Image

unit-testing-vue-3

12 min read
Last update: December 19, 2021

Testing API Calls

Unless you’re working with a simple static website, your Vue app is likely making API calls from within certain components. In this lesson, we’ll look at how we can test these kinds of data-fetching components.

The first thing to understand about testing components that make API calls is that we don’t want to be making real calls out to our backend. Doing so would couple our unit tests to the backend. This becomes an issue when we want to execute our unit tests in Continuous Integration. Real backends can also be unreliable, and we need our tests to behave predictably.

We want our tests to be fast and reliable, and we can achieve that by mocking our API calls and simply focusing on the inputs and outputs of the component we’re testing. In this lesson we’ll be using axios, the popular promise-based HTTP client, to make our calls. This means we’ll have to mock axios’ behavior, which we’ll do in a moment. But first let’s take a look at the starting code.


The Starting Code

For the sake of simplicity, instead of plugging into a full backend, we’re using json-server, which gives us a fake REST API. If this library is new to you, we taught it over in our Real World Vue course. What you need to know for this lesson is: our db.json file is our database, and json-server can fetch data from it.

Our simple db has one endpoint: “message”, and this is the data we’ll be fetching.

📄 db.json

{
"message": { "text": "Hello from the db!" }
}

In our project, I’ve also added an API service layer, which will handle the actual API calls.

📁services/📄axios.js

import axios from 'axios'

export function getMessage() {
return axios.get('http://localhost:3000/message').then(response => {
return response.data
})
}

As you can see, we’ve imported axios and we are exporting the getMessage() function, which makes a get request to our endpoint: http://localhost:3000/message, then we’re returning the data from the response.

Now that we understand how and where we’re pulling our data from, we can look at the component that triggers this API call, and displays the returned data.

📄MessageDisplay.vue

<template>
    <div>
        <p v-if="error" data-testid="message-error">{{ error }}</p>
        <p v-else data-testid="message">{{ message.text }}</p>
    </div>
</template>

<script>
    import {
        getMessage
    } from '@/services/axios.js'

    export default {
        data() {
            return {
                message: {},
                error: null
            }
        },

        async created() {
            try {
                this.message = await getMessage()
            } catch (err) {
                this.error = 'Oops! Something went wrong.'
            }
        }
    }
</script>

In the <script> section, we’ve imported the getMessage function from our axios.js file, and when our component is created, it calls getMessage using async / await since axios is asynchronous and we need to wait for the promise it returns to resolve. When it resolves, we’re setting our component’s local message data equal to the resolved value, which gets displayed in the template.

We’re also wrapping the getMessage call with try in order to catch errors that might happen, and if one does occur, we’re setting our local error data accordingly and displaying that error.


Inputs & Outputs

Looking at the MessageDisplay.vue component, what are the inputs and outputs that we’ll need to consider when writing our test?

Well, we know that the response from the getMessage call is our input, and we have two possible outputs:

  1. The call happens successfully and the message is displayed
  2. The call fails and the error is displayed

So in our test file, we’ll need to:

  1. Mock a successful call to getMessage, checking that the message is displayed

  2. Mock a failed call to getMessage, checking that the error is displayed

Let’s get started with learning how to mock axios.


Mocking Axios

Let’s scaffold the test block, import the component we’re testing, mount it, and use comments to piece apart what our tests need to be doing.

📄 MessageDisplay.spec.js

` `` jsx import MessageDisplay from '@/components/MessageDisplay' import { mount } from '@vue/test-utils'

describe('MessageDisplay', () => {
    it('Calls getMessage and displays message', async () => {
        // mock the API call
        const wrapper = mount(MessageDisplay)
        // wait for promise to resolve
        // check that call happened once
        // check that component displays message
    })

    it('Displays an error when getMessage call fails', async () => {
        // mock the failed API call
        const wrapper = mount(MessageDisplay)
        // wait for promise to resolve
        // check that call happened once
        // check that component displays error
    })
})
``
`

So let’s fill out these tests, one by one. Looking at the test where it “Calls getMessage and displays message”, our first step is to mock axios. Again, when testing components that make API calls, we don’t want to be making actual calls out to our database. We can get away with simply pretending we made the call by mocking that behavior, using Jest’s mock function.

In order to mock our API call, we’ll first import the getMessage function from our axios.js file. We can then feed that function to jest.mock() by passing it the path for where that function lives.

📄 MessageDisplay.spec.js

` `` jsx import MessageDisplay from '@/components/MessageDisplay' import { mount } from '@vue/test-utils' import { getMessage } from '@/services/axios'

jest.mock('@/services/axios')

    ...
})
``
`

You can think of jest.mock as saying: “I’ll take your getMessage function, and in return I’ll give you a mocked getMessage function.” Now, when we call getMessage within our tests, we’re actually calling the mocked version of that function, not the actual one.

So let’s call our newly mocked getMessage function from within our first test.

📄 MessageDisplay.spec.js

` `` jsx import MessageDisplay from '@/components/MessageDisplay' import { mount } from '@vue/test-utils' import { getMessage } from '@/services/axios'

jest.mock('@/services/axios')

describe('MessageDisplay', () => {
    it('Calls getMessage and displays message', async () => {
        const mockMessage = 'Hello from the db'
        getMessage.mockResolvedValueOnce({
            text: mockMessage
        }) // calling our mocked get request
        const wrapper = mount(MessageDisplay)
        // wait for promise to resolve
        // check that call happened once
        // check that component displays message
    })
})
``
`

By using jest’s < a href = "https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue" target = "blank" > mockResolvedValueOnce() < /a> method, we’re doing exactly what the method name suggests: pretending to make the API call and returning a mocked value for the call to resolve with. As its argument, this method takes in the value we want this mocked function to resolve with. In other words, this is where we put a stand-in for what the request should’ve returned. So we’ll pass in { text: mockMessage } to replicate what the server would respond with.

As you can see, we’ re using `async`
like we have in previous tests, because axios(and our _mocked_ axios call) is asynchronous.This means that before we write any assertions, we’ ll need to make sure that the promise that our mocked call returns gets resolved.Otherwise, our tests would run before the promise is resolved, and fail.

    -- -

    # # Awaiting Promises

When figuring out where to `await` in our test, we have to think back to how `getMessage`
is being called in the component we’ re testing.Remember, it’ s being called on the component’ s `created`
lifecycle hook ?

    **
    📄MessageDisplay.vue **

    ``
`jsx

async created() { try { this.message = await getMessage() } catch (err) { this.error = 'Oops! Something went wrong.' } } ` ``

Since vue - test - utils doesn’ t have access to the internals of promises that are enqueued by the `created`
lifecycle hook, we can’ t really tap into anything to `await`
for that promise.So the solution here is to use a third - party library called[flush - promises](https: //www.npmjs.com/package/flush-promises) which allows us to—well—flush the promises, ensuring they’re all resolved prior to running our assertions.

        Once we’ ve installed the library with `npm i flush-promises --save-dev`, we’ ll
        import it into our testing file and `await`
        the flushing of the promises.

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

import MessageDisplay from '@/components/MessageDisplay' import { mount } from '@vue/test-utils' import { getMessage } from '@/services/axios' import flushPromises from 'flush-promises'

jest.mock('@/services/axios')

describe('MessageDisplay', () => { it('Calls getMessage once and displays message', async () => { const mockMessage = 'Hello from the db' getMessage.mockResolvedValueOnce({ text: mockMessage }) const wrapper = mount(MessageDisplay)

await flushPromises()
// check that call happened once
// check that component displays message

}) }) ` ``

        Now that we’ ve ensured promises will be resolved before our assertions are run, we can write those assertions.

        -- -

        # # Our Assertions

        First up, we’ ll want to make sure our API call is only happening once.

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

it('Calls getMessage once and displays message', async () => { const mockMessage = 'Hello from the db' getMessage.mockResolvedValueOnce(mockMessage) const wrapper = mount(MessageDisplay)

await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1) // check that call happened once
// check that component displays message

}) ` ``

        We’ re simply running the method `.toHaveBeenCalledTimes()`
        and passing in the number of times we expect `getMessage`
        to have been called: `1`.Now we’ ve ensured that we aren’ t accidentally hitting our server more times than we should be.

        -- -

        Next up, we need to check that our component is displaying the message it received from our `getMessage`
        request.In the ** MessageDisplay ** component’ s template, the `p`
        tag that displays the message has an id to be used
        for tests: `data-testid="message"`

        **
        📄MessageDisplay.vue **

        ``
        `html
` ``
        We learned about these testing ids in the previous lesson.We’ ll use that id to `find`
        the element, then assert that its text content should be equal to the value our mocked `getMessage`
        request resolved with: `mockMessage`

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

it('Calls getMessage once and displays message', async () => { const mockMessage = 'Hello from the db' getMessage.mockResolvedValueOnce({ text: mockMessage }) const wrapper = mount(MessageDisplay)

await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const message = wrapper.find('[data-testid="message"]').text()
expect(message).toEqual(mockMessage)

}) ` ``

        If we run `npm run test:unit` in the terminal, we’ ll see our newly written test is passing!We can now move on to our second test, where we’ ll mock a _failed_ `getMessage`
        request and check that our component is displaying the error.

        -- -

        # # Mocking a failed request

        The first step, of mocking the failed API call, is very similar to our first test.

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

it('Displays an error when getMessage call fails', async () => { const mockError = 'Oops! Something went wrong.' getMessage.mockRejectedValueOnce(mockError) const wrapper = mount(MessageDisplay)

await flushPromises()
// check that call happened once
// check that component displays error

}) ` ``

        Notice how we’ re using `mockRejectedValueOnce`
        to simulate the failed get request, and we’ re passing it the `mockError`
        for it to resolve with.

        After awaiting the flushing of the promises, we can then check that the call only happened once and verify that our component’ s template is displaying the expected `mockError`.

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

it('Displays an error when getMessage call fails', async () => { const mockError = 'Oops! Something went wrong.' getMessage.mockRejectedValueOnce(mockError) const wrapper = mount(MessageDisplay)

await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const displayedError = wrapper.find('[data-testid="message-error"]').text()
expect(displayedError).toEqual(mockError)

}) ` ``

        Just like our first test, we’ re using `.toHaveBeenCalledTimes(1)`
        to make sure we’ re not making the API call more than we should be, and we’ re finding the element that displays the error message and checking its text content against the `mockError`
        that our mocked failed request returned.

        Now
        if we run these tests, what happens ? The test is failing :

        **
        Expected number of calls: 1 Received number of calls: 2 **

        Hmm… what’ s happening here ? Well, in our first test, `getMessage`
        was called, and then it gets called again in our second test.We haven’ t done anything to clear out our mocked `getMessage`
        function before running the second test.Fortunately, the fix is quite simple.

        -- -

        # # Clear All Mocks

        Below where we’ re creating our jest mock, we can add the solution, clearing all of our mocks.

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

jest.mock('@/services/axios') beforeEach(() => { jest.clearAllMocks() }) ` ``

        Now, `beforeEach`
        test is run, we’ ll make sure the `getMessage`
        mock has been cleared, which will reset the number of times it’ s been called back to 0.

        Now, when we run our tests, they’ re all passing.Great work!

        -- -

        # # The Full Code

        **
        📄MessageDisplay.spec.js **

        ``
        `jsx

import MessageDisplay from '@/components/MessageDisplay' import { mount } from '@vue/test-utils' import { getMessage } from '@/services/axios' import flushPromises from 'flush-promises'

jest.mock('@/services/axios') beforeEach(() => { jest.clearAllMocks() })

describe('MessageDisplay', () => { it('Calls getMessage and displays message', async () => { const mockMessage = 'Hello from the db' getMessage.mockResolvedValueOnce({ text: mockMessage }) const wrapper = mount(MessageDisplay)

await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)

const message = wrapper.find('[data-testid="message"]').text()
expect(message).toEqual(mockMessage)

})

it('Displays an error when getMessage call fails', async () => { const mockError = 'Oops! Something went wrong.' getMessage.mockRejectedValueOnce(mockError) const wrapper = mount(MessageDisplay)

await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const displayedError = wrapper.find('[data-testid="message-error"]').text()
expect(displayedError).toEqual(mockError)

}) }) ` ``

        -- -

        # # Let’ s ReVue

        We’ ve learned that when testing API calls, the same essential rules apply : focus on the component’ s inputs(the request’ s response) and outputs(the message or error that’ s displayed),
        while being conscious of avoiding tight coupling between the test and the implementation details of the component(finding the element by its testing id versus finding it by its element type,
            for example).We also learned how to use jest to mock our calls and the third party library flush - promises to await asynchronous behavior in our lifecycle hooks.

        In the next lesson, we’ ll learn what the heck a stub is and how it can help us test parent components.