Tips for Unit Testing Vue Components with Jest
Using Jest to unit test Vue.js components can be tricky. We need a separate Vue Test Utils (@vue/test-utils)
scoped package in order to virtually mount our components and use Jest to execute the tests. Because these two libraries work together, it’s important to make sure we don’t get confused about which API calls belong to which library. Along with these libraries, we also need to pay attention to JSDOM (virtual browser environment) specific methods, which comes with Jest. Having to juggle all this may be confusing and can discourage us from writing unit tests.
E.g., shallowMount()
is a Vue Test Utils method to create a shallow wrapper
component (using shallow rendering to avoid rendering children components), and beforeEach()
is a Jest method which executes the callback argument before each of the tests. We run shallowMount()
inside beforeEach()
so a component is mounted before every test.
I made a list of common testing methods that my team used to help successfully unit test our Vue components. Hope this will help others going through similar process.
Initial Setup
Let’s start by setting up Jest. Install Jest npm package as devDependencies
$ npm i jest -D
Now let’s install Vue Test Utils and other dependencies like babel-jest
, vue-jest
etc. (this is straight from the Vue Test Utils docs)
$ npm i @vue/test-utils vue-jest babel-jest -D
Once we have all packages setup, create a Jest config file jest.config.js
in the root folder of our project. As an alternative, we can also add JSON object inside module.exports
to a jest: {}
property to package.json
which cuts down on the number of config files we have to manage.
Make sure .babelrc
file has config below with babel-preset-env
module installed.
Once we have setup the configs with options (mainly collectCoverageFrom
source path) we are ready to run Jest!
Now all we need to do is add scripts: { “test”: “jest” }
to package.json. Then run that script.
$ npm run test
Writing Test Files
The above script is not very useful until we have test files that Jest can run, so let’s add one.
Jest will pick up *.test.js
or *.spec.js
files in our project folder. I like to place test files in the same folder as my Vue components and use component file name. e.g. button.vue
component will have button.test.js
test file in components/
folder. This helps me manage component files and test files since they live the same folder and are right next to each other in the Explorer
in VSCode.
Test File Template
Here is a base template test file I use to get started. Note shallowMount()
has various properties like propsData, mocks, stubs, methods
we can set to our mounted component to mock or stub various properties of the component. Common practice is to invoke shallowMount()
and store mocked component in a wrapper
before every test and destroy wrapper
after each test. That way we are starting with a fresh state every test, which makes the test more predictable.
Jest has describe()
, test()
and expect()
testing functions to setup each test and asset expected value. I like to check wrapper.isVueInstance
test passes in the beginning to make sure I have mocked default properties for the component.
If our component uses Vuex, we need to add Vuex to a Vue instance (named localVue
here) using use()
and pass the instance into shallowMount()
.
Common Testing Methods
Let’s look at some testing methods that cover most types of unit testing.
- Existence of DOM elements
We begin the test by checking ifwrapper
has all default elements we expect to render when component is mounted. We do this by checking for actual element tag or other attributes like class, id etc. Jest’s assertion functions liketoBe()
along withwrapper.contains()
orwrapper.findAll().length
from Vue Test Utils can help with this test.
We can also check for innerHTML
of elements using .text()
function of the DOM element that wrapper.find()
returns.
e.g. expect(wrapper.find('.blah').text()).toBe('blah text')
.
- DOM action events
We can test the events that should emit when an action is performed in the component. e.g. when a close button is clicked, a dialog should be close by listening to aclose
event. We usetrigger()
function from Vue Test Utils to trigger an event on the wrapper’s selected DOM element andwrapper.emitted()
gives a list of emitted events which is used to check for existence desired event.
Above we trigger click
event on closeBtn
DOM selector. We can also trigger other events like keydown
for keypress etc.
- Accessing Vue wrapper properties
In case we need to access or changedata
,computed
,methods
andprops
of our Vue component, we can usewrapper.vm
object since Vue Test Utils creates an instance of the component inwrapper
. This is helpful when we have to mock values for one of the properties.
wrapper.vm.name = 'test'; // changes name data propertywrapper.vm.save(); // invokes save() methodwrapper.setProps({ propsName: newValue }); // changes propsName to newValue
One exception is computed
properties, which need to be remounted using shallowMount()
and passed in as a property. Since we mount our component in beforeEach()
, we can store the shallowMount()
in a factory function which can be easily called during beforeEach()
and in test that needs to pass in overrides for computed
properties as getter functions.
- Mocking methods and modules
One of the important things to know when writing unit tests is how to fake or mock the execution of a module or method that are not part of the unit we are testing. This helps make our tests more predictable and also avoids any external code changes from failing our tests. We can use Jest’sfn()
andmock()
functions to return mocked versions. There is also amocks
property insideshallowMount()
in Vue Test Utils which is use to mock any global functions that our component might be using without import statement.
Let’s look at mocking functions.
Above test, we use jest.fn()
to mock the return value of various functions. We can pass an argument inside fn(() => true)
that gets invoked for the mock. If left empty, an undefined
function is returned.
We also mocked window.open()
global function because JSDOM doesn’t provide but our test needs to execute it.
These functions are also spies
which lets us spy on their behavior and use expect().toBeCalled()
functions to verify they are invoked in the test. We can also use jest.spyOn(Object, function name)
Jest function for similar behavior.
Other helpful function mocks include setInterval
and setTimeout
since Javascript native timers don't work as expected in Jest. We can use Jest’s functions like advanceTimersByTime(x)
or runOnlyPendingTimers()
to clear all timers, just make sure to declare jest.userFakeTimers()
at the top of test file.
jest.useFakeTimers(); // Declare at top of test filejest.advanceTimersByTime(1000); // Inside test function if you have specific millisjest.runOnlyPendingTimers(); // Fast-forward until all pending timers have been executed
Mocking imported module is quite simple.
If we have a Constants
module imported in our component, then we return the mocked object with any properties our component uses, e.g. Constants.myConstant
.
- Async callbacks
If we have asynchronous code in the code block of our component, we can useasync/await
toawait
the async callback, usually a Promise.
Demo
Link to vue-jest-unit-test repo I setup with an App
component along with its tests.
Conclusion
If we have a good handle of Jest and Vue Test Utils APIs, then writing unit tests for our Vue components will seem like a breeze, we may even enjoy it! And that’s important because unit testing our components helps us write better designed and easily refactorable code.