Testing actions in the context of a component is correctly dispatching them is discussed in this article.
I am working on a open source book about testing Vue applications. It covers Vue components, Vuex and Vue Router. The source and contribution guides (anyone is welcome!) are here and the book is here.
The source code for the test described in this article can be found here.
We will write a action that follows a common Vuex pattern:
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.
block1.js
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:
Let’s go ahead and write the test, and let the failure messages guide us.
block2.js
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)
})
})
view raw
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:
block3
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 it’s behavior. Jest provides ES6 Class Mocks, which are a perfect fit for mocking axios
.
The axios
mock looks like this:
block4.js
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 unit:test
now yields a passing test!
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:
block5.js
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:
block6.js
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:
block7.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:
block8.js
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.
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 using a manual mock, 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.
This guide discussed:
The source code for the test described on this page can be found here.
Recommended Courses:
Learn Web Development Using VueJS
☞ http://bit.ly/2q2qUv1
Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)
☞ http://bit.ly/2H56b0Q
Vue JS 2.0 - Mastering Web Apps
☞ http://bit.ly/2H5NudD
☞ Test Driven Development with Angular
☞ Vue js Tutorial Zero to Hero || Brief Overview about Vue.js || Learn VueJS 2023 || JS Framework
☞ Learn Vue.js from scratch 2018