Since a router usually involves multiple components operating together, often routing tests take place further up the testing pyramid, right up at the e2e/integration test level. However, having some unit tests around your routing can be beneficial as well.
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.
There are two ways to test components that interact with a router:
- Using an real router instance
- Mocking the
$route
and$router
global objects
Since most Vue applications use the official Vue Router, this guide will focus that.
The source code for the tests described on this page can be found here and here.
We will build a simple <App>
, that has a /nested-child
route. Visiting /nested-child
renders a <NestedRoute>
component. Create an App.vue
file, and insert the following minimal component:
https://gist.github.com/b74b4968d8dd450d32aa0cc4028b1cfd
<NestedRoute>
is equally as minimal:
https://gist.github.com/ea1f6fa220f42f21413721080358dc63
In a real app, you normally would create a router.js
file and import the routes we made, and write something like this:
https://gist.github.com/618155d8372fc198442bccaaaedcf996
Since we do not want to polluate the global namespace by calling Vue.use(...)
in our tests, we will create the router on a test by test basis. This will let us have more fine grained control over the state of the application during the unit tests.
Let's look at some code, then talk about what's going on. We are testing App.vue
, so in App.spec.js
add the following:
https://gist.github.com/287460532914aea711df803c85dfef1f
As usual, we start by importing the various modules for the test. Notably, we are importing the actual routes we will be using for the application. This is ideal in some ways - if the real routing breaks, the unit tests should fail, letting us fix the problem before deploying the application.
We can use the same localVue
for all the <App>
tests, so it is declared outside the first describe
block. However, since we might like to have different tests for different routes, the router is defined inside the it
block.
Another notable point that is different from other guides in this book is we are using mount
instead of shallowMount
. If we use shallowMount
, <router-link>
will be stubbed out, regardless of the current route, a useless stub component will be rendered.
Using mount
is fine in some cases, but sometimes it is not ideal. For example, if you are rendering your entire <App>
component, chances are the render tree is large, containing many components with their own children components and so on. A lot of children components will trigger various lifecycle hooks, making API requests and the such.
If you are using Jest, its powerful mocking system provides an elegent solution to this problem. You can simply mock the child components, in this case <NestedRoute>
. The following mock can be used and the above test will still pass:
https://gist.github.com/48b95a131d7794e46f2db90b17962e37
Sometimes a real router is not necessary. Let's update <NestedRoute>
to show a username based on the current path's query string. This time we will use TDD to implement the feature. Here is a basic test that simply renders the component and makes an assertion:
https://gist.github.com/7553f0db86875c907ac518235cb65840
We don't have a <div class="username">
yet, so running the test gives us:
https://gist.github.com/8fc6f13b3d174dcf674d786c6abffaee
Now the test fails with:
https://gist.github.com/366c81d8adce09dfc6549014d933518f
This is because $route
does not exist. We could use a real router, but in this case it is easier to just use the mocks
mounting option:
https://gist.github.com/af236af57f7d4e788c2bc43f44efc9df
Now the test passes. In this case, we don't do any navigation or anything that relies on the implementation of the router, so using mocks
is good. We don't really care how username
comes to be in the query string, only that it is present.
Often the server will provide the routing, as opposed to client side routing with Vue Router. In such cases, using mocks
to set the query string in a test is a good alternative to using a real instance of Vue Router.
Vue Router provides several types of router hooks, called "navigation guards". Two such examples are:
- Global guards (
router.beforeEach
). Declared on the router instance. - In component guards, such as
beforeRouteEnter
. Declared in components.
Making sure these behavae correctly is usually a job for an integration test, since you need to have a user navigate from one route to another. However, you can also use unit tests to see if the functions called in the navigation guards are working correctly and get faster feedback about potential bugs. Here are some strategies on decouple logic from nagivation guards, and writing unit tests around them.
Let's say you have a bustCache
function that should be called on every route that contains the shouldBustCache
meta field. You routes might look like this:
https://gist.github.com/3ac4a9bd4ad2916acb10b1e43f7e960a
Using the shouldBustCache
meta field, you want to invalidate the current cache to ensure the user does not get stale data. An implementation might look like this:
https://gist.github.com/e9687e3bb1cbe8378c86128d175fa44e
In your unit test, you could import the router instance, and attempt to call beforeEach
by typing router.beforeHooks[0]()
. This will throw an error about next
- since you didn't pass the correct arguments. Instead of this, one strategy is to decouple and independently export the beforeEach
navigation hook, before coupling it to the router. How about:
https://gist.github.com/1414dad055aee7e6ac29dee6094610f1
Now writing a test is easy, albeit a little long:
https://gist.github.com/927828ea5ec4036f25cbb2fa19f5c612
The main point of interest is we mock the entire module using jest.mock
, and reset the mock using the afterEach
hook. By exporting the beforeEach
as a decoupled, regular JavaScript function, it become trivial to test.
To ensure the hook is actually calling bustCache
and showing the most recent data, a e2e testing tool like Cypress.io, which comes with applications scaffolded using vue-cli, can be used.
Component Guards are also easy to test, once you see them as decoupled, regular JavaScript functions. Let's say we added a beforeRouteLeave
hook to <NestedRoute>
:
https://gist.github.com/45d38d61643dda5b04fd5ee1b354f109
We can test this in exactly the same way as the global guard:
https://gist.github.com/af6128bf4811e6f26532e237f6d4bc92
While this style of unit test can be useful for immediate feedback during development, since routers and navigation hooks often interact with several components to achieve some effect, you should also have integration tests to ensure everything is working as expected.
This guide covered:
- testing components conditionally rendered by Vue Router
- mocking Vue components using
jest.mock
andlocalVue
- decoupling global navigation guards from the router and testing the independently
- using
jest.mock
to mock a module
The source code for the test described on this page can be found here and here.