This book is written for Vue.js 2 and Vue Test Utils v1.
Find the Vue.js 3 version here.
# Testing Actions
Testing actions in isolation is very straight forward. It is very similar to testing mutations in isolation - see here for more on mutation testing. Testing that a component is correctly dispatching actions is discussed here.
The source code for the test described on this page can be found here.
# Creating the Action
We will write an action that follows a common Vuex pattern:
- make an asynchronous call to an API
- do some processing on the data (optional)
- commit a mutation with the result as the payload
This is an authenticate
action, which sends a username and password to an external API to check if they are a match. The result is then used to update the state by committing a SET_AUTHENTICATED
mutation with the result as the payload.
import axios from "axios"
export default {
async authenticate({ commit }, { username, password }) {
const authenticated = await axios.post("/api/authenticate", {
username, password
})
commit("SET_AUTHENTICATED", authenticated)
}
}
The action test should assert:
- was the correct API endpoint used?
- is the payload correct?
- was the correct mutation committed with the result
Let's go ahead and write the test, and let the failure messages guide us.
# Writing the Test
describe("authenticate", () => {
it("authenticated a user", async () => {
const commit = jest.fn()
const username = "alice"
const password = "password"
await actions.authenticate({ commit }, { username, password })
expect(url).toBe("/api/authenticate")
expect(body).toEqual({ username, password })
expect(commit).toHaveBeenCalledWith(
"SET_AUTHENTICATED", true)
})
})
Since axios
is asynchronous, to ensure Jest waits for test to finish we need to declare it as async
and then await
the call to actions.authenticate
. Otherwise the test will finish before the expect
assertion, and we will have an evergreen test - a test that can never fail.
Running the above test gives us the following failure message:
FAIL tests/unit/actions.spec.js
● authenticate › authenticated a user
SyntaxError: The string did not match the expected pattern.
at XMLHttpRequest.open (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:482:15)
at dispatchXhrRequest (node_modules/axios/lib/adapters/xhr.js:45:13)
at xhrAdapter (node_modules/axios/lib/adapters/xhr.js:12:10)
at dispatchRequest (node_modules/axios/lib/core/dispatchRequest.js:59:10)
This error is coming somewhere from within axios
. We are making a request to /api...
, and since we are running in a test environment, there isn't even a server to make a request to, thus the error. We also did not defined url
or body
- we will do that while we solve the axios
error.
Since we are using Jest, we can easily mock the API call using jest.mock
. We will use a mock axios
instead of the real one, which will give us more control over its behavior. Jest provides ES6 Class Mocks, which are a perfect fit for mocking axios
.
The axios
mock looks like this:
let url = ''
let body = {}
jest.mock("axios", () => ({
post: (_url, _body) => {
return new Promise((resolve) => {
url = _url
body = _body
resolve(true)
})
}
}))
We save url
and body
to variables to we can assert the correct endpoint is receiving the correct payload. Since we don't actually want to hit a real endpoint, we resolve the promise immediately which simulates a successful API call.
yarn test:unit
now yields a passing test!
# Testing for the API Error
We only tested the case where the API call succeed. It's important to test all the possible outcomes. Let's write a test for the case where an error occurs. This time, we will write the test first, followed by the implementation.
The test can be written like this:
it("catches an error", async () => {
mockError = true
await expect(actions.authenticate({ commit: jest.fn() }, {}))
.rejects.toThrow("API Error occurred.")
})
We need to find a way to force the axios
mock to throw an error. That's what the mockError
variable is for. Update the axios
mock like this:
let url = ''
let body = {}
let mockError = false
jest.mock("axios", () => ({
post: (_url, _body) => {
return new Promise((resolve) => {
if (mockError)
throw Error()
url = _url
body = _body
resolve(true)
})
}
}))
Jest will only allow accessing an out of scope variable in an ES6 class mock if the variable name is prepended with mock
. Now we can simply do mockError = true
and axios
will throw an error.
Running this test gives us this failing error:
FAIL tests/unit/actions.spec.js
● authenticate › catchs an error
expect(function).toThrow(string)
Expected the function to throw an error matching:
"API Error occurred."
Instead, it threw:
Mock error
It successfully caught the an error... but not the one we expected. Update authenticate
to throw the error the test is expecting:
export default {
async authenticate({ commit }, { username, password }) {
try {
const authenticated = await axios.post("/api/authenticate", {
username, password
})
commit("SET_AUTHENTICATED", authenticated)
} catch (e) {
throw Error("API Error occurred.")
}
}
}
Now the test is passing.
# Improvements
Now you know how to test actions in isolation. There is at least one potential improvement that can be made, which is to implement the axios
mock as a manual mock. This involves creating a __mocks__
directory on the same level as node_modules
and implementing the mock module there. By doing this, you can share the mock implementation across all your tests. Jest will automatically use a __mocks__
mock implementation. There are plenty of examples on the Jest website and around the internet on how to do so. Refactoring this test to use a manual mock is left as an exercise to the reader.
# Conclusion
This guide discussed:
- using Jest ES6 class mocks
- testing both the success and failure cases of an action
The source code for the test described on this page can be found here.