
Have you considered implementing unit testing in your app built on a reactive JavaScript framework? If you haven't explored this yet, I've got you covered. I faced some challenges myself when starting out, but I've put together a helpful guide with code solutions to help you get started with unit testing.
Unit Testing
TL;DR: Want to skip to the code solutions? Just grab the link here: https://github.com/kjugi/meeting-app-unit-tests-playground
Our app to test should contain the following stack: reactive framework, state management and Jest. Let's assume that in our case we are using Vue together with vue-test-utils and Vuex.
While writing unit tests we should ask ourselves one question repeatedly: Can I fake this data?
If you keep this question in mind when writing tests, everything should go smoothly! But of course, there are some common problems that come up during this process, so I will show you how to resolve them.
1. Data mocking is the first and most important rule for unit tests. Without it, tests could be worthless. We can mock almost everything and I highly recommend checking out their documentation — you can find here different kinds of mocks and specific examples when and how to use them.
We should mock API calls and any requests responses by mocking Axios. Then we can mock functions, and expect for some results just to test our case. Here’s a small example:
import { mount } from '@vue/test-utils'
import axios from 'axios'
jest.mock('axios') // Mocking axios here
describe('test', () => {
beforeEach(() => {
// Clearing before run test to get fresh response in each case
axios.get.mockClear()
})
it('case', () => {
// Mocking returned value
const response = { data: [{ key: val }] }
axios.get.mockResolvedValue(response)
const wrapper = mount(Component)
// Need to resolve pending promise
...
expect(axios.get)... // Checking our request
})
})
2. Promises and asynchronous code cause a little pain when we need to test an app. Luckily we don’t need to worry about it there because when you are writing new tests you can simply use async/await syntax. In a test it looks like this:
import { mount } from '@vue/test-utils'
describe('your test', () => {
it('case', async () => {
const wrapper = mount(Component)
await wrapper.find('#el').setChecked(false)
...
await wrapper.setData({..})
expect(...)
})
})
3. Timer functions, like setTimeout, can be resolved with one line, using provided by Jest timer mock function, which let us run tests quicker, without waiting for real time to elapse.
A similar approach should be used while working with promises, for example, network requests, they should be resolved as fast as possible, it can be achieved with one simple npm package called flush-promises.
import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
jest.useFakeTimers()
describe('test', () => {
it('case', async () => {
const wrapper = mount(Component)
// we need to wait for some request with data on start
await flushPromises() // Resolving all pending promises
expect(...)
// assume that we are hidding message after resolve promise after 5 seconds
expect(...) // checking is it visible
jest.runAllTimers() // Fast-forward for all starded timers
expect(...) // now we can check that it's hidden
})
})
4. Complicated, long and unreadable test cases can be fixed by introducing factory functions to our code.
It is a great feature in testing which saves our time and keeps tests short and clean. You can create a local factory function in your test file or go with one global factory function and import it across a whole app when your code is really convenient.
To avoid complicated tests, we should also keep each test case as short as we can. The basic rule to keep is one test case for one method, action or emit.
It’s worth considering grouping lanes with arrange/act/assert pattern.
- Arrange section sets the necessary information to process the test
- Act means performing some actions
- Assert verifies that the results are as expected.
Example for arrange, act and assert pattern:
import { mount } from '@vue/test-utils'
describe('test', () => {
it('case', () => {
// arrange
const wrapper = mount(Component, { propsData: { a: true }})
// act
wrapper.setProps({ a: false })
// assert
expect(wrapper.vm.a).toBe(false)
expect(...)
})
})
Example for factory function:
import { mount } from '@vue/test-utils'
// local factory function
const factory = () => {
return mount(Component, {
propsData: {
repeatedData: 'inEachTestCase'
}
})
}
describe('test', () => {
it('case', () => {
const wrapper = factory()
...
})
})
5. State management is the next station for us. How do we test it? The answer is in distinct files. It is separated from component tests. I suggest testing each type of store element separately or split them by functionality/store modules.
In fact, in components, we should forget about dispatches and commits. You probably guessed that we should mock it and you would be right.
File structure with store modules:
// split by store modules
cart.js
account.js
// split by store module and functionality (for bigger stores)
cartActions.spec.js
cartGetters.spec.js
accountActions.spec.js
Testing mutation, action and getter:
describe('test', () => {
it('mutation example', () => {
const variable = true
const state = { someValue: false }
// our real mutation imported from file
mutations.changeValue(state, variable)
expect(state.someValue).toBe(true)
})
it('action example', () =>
const context = { commit: jest.fn() }
const item = { 'test': true }
// our real action again which contain commit call inside
actions.doSomeAction(context, item)
expect(context.commit).toHaveBeenCalledWith('addMeeting', testItem) // checking is context called
})
it('getter example', () =>
const state = { posts: [...] }
// real getter which returns only featured posts
const result = getters.getFeaturedPosts(state)
expect(result).toHaveLength(2)
})
})
6. Date based elements are one of the toughest problems to solve. I mean, you could simply mock Date.now in Jest but if you need to mock some static data in new Date() could be really challenging. Fortunately, you can easily use an npm package for that: jest-date-mock.
This is a basic package which will mock the data provided by us to function and we can revert this data when we finish our test.
import { mount } from '@vue/test-utils'
import { advanceTo, clear } from 'jest-date-mock'
describe('test', () => {
beforeEach(() => {
advanceTo(new Date('2000-01-01T00:00:05Z'))
})
it('case', () => {
// Assume we have computed which returns cut date to element
const wrapper = mount(Component)
expect(wrapper.find('#dateEl').element.value).toBe('2000-01-01')
})
afterEach(() => {
clear() // Clearing the time to real value
})
})
That’s all for now. I think that these six points are enough to get you started unit testing and inspiring to create some new tests in your app. Let me know in the comments how it goes.
All described issues and more can be found in this demo repository. There’s also a practice directory to challenge yourself in unit tests.
Take care, Filip — Front-end developer at SNOW.DOG