Unit Testing a Vue Component with Vuex Using Vue Test Utils and Jest

Testing is crucial, as we all know. When we develop some projects and implement some features on them, we have to keep our codebase robust and maintainable, and also, we shouldn’t allow introducing new bugs through this coding process. On the other hand, testing is also helpful to refactor our codebase without a headache. So I think every programmer has to learn how to write the test cases and, so importantly know which parts of writing the test.

Abdulsamet İLERİ
7 min readFeb 9, 2020

Let’s start. We have a login component. We are getting the credentials from the user and will call the login endpoint within a specified vuex action. I’m using axios for ajax calls and Vuelidate for validation purposes. I don’t mention these libraries' details because we focus on writing tests for the login component. But I recommend checking these out.

Login Component

I am also skipping the HTML part of this component. It includes basic bootstrap vue form and input components.

Here we described our user credentials and also some validation rules for these. If the user provides some data and clicks the login button, firstly, we check validations are provided, and if so, we dispatch this request with their information.

our user store

After that, our [LOGIN] action calls login API and gets the result and destructures token from it, writes this token to the cookie, commits SET_TOKEN mutation, and resolves happily.

[LOGIN] syntax seems strange when the first time. It is nothing but ES6 dynamic property syntax. You can search for it.

Let’s write our unit tests.

We use vue test utils and Jest. I suppose you have basic knowledge of using these two. So we won’t focus on learning to use their functions.

Before writing our unit tests, we describe our helper methods for creating wrapper and storing objects. We also use localVue to avoid polluting our main Vue constructor. You can read more about localVue.

Firstly we create our localVue instance with BootstrapVue (for rendering b-form-input, b-form correctly, I mean without a problem), Vuex for store, and Vuelidate for our validation methods and properties touch() and $invalid.

const localVue = createLocalVue()
localVue.use(BootstrapVue)
localVue.use(Vuex)
localVue.use(Vuelidate)
function createStore (overrides) {
const defaultStoreConfig = {

}
return new Vuex.Store(
merge(defaultStoreConfig, overrides)
)
}

We define create store helper method. It’s important that the merge comes from the great library called loadash. As its name indicates, merges objects. We use to override defaultStoreConfig by providing parameters to this function.

function createWrapper (overrides) {
const defaultMountingOptions = {
localVue,
mocks: {
$router: { push: jest.fn() }
},
store: createStore()
}
return mount(Login, merge(defaultMountingOptions, overrides))
}

We also define the create wrapper method. The wrapper is a special concept. It includes our Vue instance (VM) and some helper function like (html(), find() so on.) It simplifies simulating, and traversing on dom objects. To get the wrapper object provided by the Vue-test-utils library, we use the mount function. We can also use shallowMount.

We provide localVue to our Vue instance. Remember that we injected BootstrapVue, Vuex, and Vuelidate. We have to do this to work with external plugins.

It’s important to note that, as you can see, we mock $router object. But why? Remember that we use $router.push in handleLogin method.

We mock this to avoid getting an undefined $router variable error when we run our tests. Also, we don’t care about $push works or not. This is not our responsibility. It’s vue’s job. We can easily mock $push function by using jest.fn().

We also inject our store to mount to test our store.

We give the first parameter to mount() function of our Login.vue component. We are done! Let’s move on.

describe('Login.vue', () => {
let wp
beforeEach(() => {
wp = createWrapper()
})

To start all unit tests with the clear wrapper object, we initialize our wrapper within the beforeEach method. As you remember and its name indicates, beforeEach works before every unit test run. So we can test clearly without polluting our wrapper (our instance). I mean, a test can not affect the other one. It’s really important to provide this for unit tests. We have to make sure all our unit tests work within isolated situations.

Our first unit test is simply a snapshot test. Thanks to the snapshot test, we ensure our login component UI does not change unexpectedly.

it('html should render correctly', () => {
expect(wp.html()).toMatchSnapshot()
})

In the second one, we assert user clicks the submit button immediately without entering the email or password. Let’s test it.

it('After the user submit button, $v.$invalid return true?', () => {
const wp = createWrapper()
const loginBtn = wp.find('button')
loginBtn.trigger('submit')
expect(wp.vm.$v.$invalid).toBeTruthy()
})

Firstly, we are getting a wrapper object, and we choose our submit button with a find method provided by the wrapper helper method. After we get our submit button reference, we simulate submit action using the trigger and expect $invalid is true. If we accidentally delete $v.$invalid line in our Login component or we add not operator (!), this test will be failed. Why is this important? Because our validation rules are checked by this property. But this is an implementation detail. It’s not in the scope of this article.

And the other one

it('After login dispatches successfully, should go / route?', async () => {
expect.assertions(3)

let actions = { [LOGIN]: jest.fn().mockResolvedValue() }

const store = createStore({ actions })

const wp = createWrapper({ store })
wp.setData({ credentials: { email: 'test@test.com', password: 'huhu', rememberMe: true } })

const loginBtn = wp.find('#submitBtn')
loginBtn.trigger('submit')

await flushPromises()

expect(actions[LOGIN]).toHaveBeenCalled()
expect(wp.vm.$router.push).toHaveBeenCalled()
expect(wp.vm.$router.push.mock.calls[0][0]).toEqual({ path: '/' })
})

I can explain line-by-line.

expect.assertions(3)

Firstly, we expect this method to assert our three conditions. Notice that three expected conditions place at the bottom of the function.

let actions = { [LOGIN]: jest.fn().mockResolvedValue() }
const store = createStore({ actions })
const wp = createWrapper({ store })

We mock our login action because we don’t interested in its implementation details we don’t care about it. We assume that is works as expected. We initialize our store with our mock login action method using our helper createStore function. Notice that, helper functions are really useful for these cases. Our code stays clean and maintainable. We can customize easily how we want. After that, we create our wrapper object.

wp.setData({ credentials: { email: 'test@test.com', password: 'huhu', rememberMe: true } })
const loginBtn = wp.find('button')
loginBtn.trigger('submit')

We define user credentials as manually using setData helper method. And also we find our submit button within the dom tree and simulate click behavior.

await flushPromises()

if we cannot await all promises, our unit test function exits when the promise is pending so we cannot test our function behavior. In order to stop this behavior, we have to wait for all the promises to flush. flushPromises method is nothing but flushing all pending resolved promise handlers. It’s important to put our async tests.

expect(actions[LOGIN]).toHaveBeenCalled()
expect(wp.vm.$router.push).toHaveBeenCalled()
expect(wp.vm.$router.push.mock.calls[0][0]).toEqual({ path: '/' })

after that, we expect our login action to be called, also $router.push is called with the correct argument path / (home route). If we change the path for example as /about in the login component, we get an error.

And the last one is testing our complex login action method.

jest.mock('@/api/auth.js', () => {
return {
login: jest.fn().mockResolvedValue({ data: { token: 'token' } })
}
})

describe('actions', () => {
it('after login token is assigned successfully?', async () => {
const context = {
commit: jest.fn()
}
const body = {
login: 'login',
password: 'password'
}
actions[LOGIN](context, body)
await flushPromises()
expect(context.commit).toHaveBeenCalledWith(SET_TOKEN, 'token')
})
})

We explain line-by-line.

jest.mock('@/api/auth.js', () => {
return {
login: jest.fn().mockResolvedValue({ data: { token: 'token' } })
}
})

Firstly, we mock our login API call. Our login API method is in ‘@/api/auth.’ We assume that it works as expected with the correct response. It’s important to note that in a unit test every asynchronous function must be mocked. (It’s not reasonable to wait for a real API call in the unit test.). We define our token with the name ‘token’. It’s important we will ensure that SET_COMMIT is called with the ‘token’ argument. Wait for it.

const context = {
commit: jest.fn()
}
const body = {
login: 'login',
password: 'password'
}

We also mock our first parameter context object within the commit function. We don’t care about its functionality. It’s Vue’s responsibility. We also assume that we get the correct credentials from the user.

After that

actions[LOGIN](context, body)

we invoke our login action with the correct parameters to inspect its behavior.

await flushPromises()

we are waiting to flush all resolved promises as explained above.

expect(context.commit).toHaveBeenCalledWith(SET_TOKEN, 'token')

We assure that commit(SET_TOKEN, ‘token’) line is invoked and works as expected. Remember that we define token as our API mock function. If we change ‘token’ with ‘another string’ we get an error.

It’s all done. Thank you for all. This is my experience. I wanted to share it with you. Again it’s enough for me you get some ideas from this article.

--

--