Demo of how to unit test vue-hackernews-2.0 (with Jest and vue-test-utils)

Nowadays modern libraries like React or VueJs and testing tools like Jest or Cypress.io allow us to test our web apps in ways that were impossible some years ago. However, the examples that we usually find in the web are too simple and testing a relatively complex app can be challenging for frontend developers without a strong testing background.

In this incredible post, Gleb Bahmutov (one of the Cypress developers), showed how easy was to cover one of the most popular VueJS examples (VueJs HackerNews clone) with end-to-end tests and he threw this question:

Would the entire experience of unit testing this app be full of pain and misery?

I ❤️ Cypress.io and use it in my projects to cover my critical happy paths with end-to-end tests, however, I believe that unit tests are also essential for Real World™ frontend apps because they help us to better understand and design our apps with a much faster feedback loop.

A fast feedback loop is critical to code and refactor without fear

I want to show that unit testing a non-trivial VueJs app is not an easy task but it brings us lots of benefits and it can also be funny 😉

TL;DR

In this post you’ll see how to unit test the Vue.js HackerNews clone made by Evan You, which uses vue-router & vuex, nuxt server-side rendering and a firebase backend.

These are the main topics that we will cover in this post:

Test page components

Stub the backend API using fake data

Stub Vue router

Test the interaction with Vuex actions

Mock nuxt asyncData hooks

You can find the forked repo with tests here.

Understanding inputs and outputs of a component

If we imagined a component as a black box function with some inputs and some outputs we could try to test it from the outside without accessing its internals but, which would be the inputs of a component?

Props : we can pass params as props

: we can pass params as props Route : we can pass route params

: we can pass route params DOM : We can interact with the UI by clicking a link, filling an input or scrolling the page

: We can interact with the UI by clicking a link, filling an input or scrolling the page VUEX store: as the single point of truth, the store can be accessed by the component to get some of its inputs

And which would be the outputs of a component?

DOM: The component can re-render its UI producing a new output.

The component can re-render its UI producing a new output. VUEX store : the component can dispatch actions with mutations that will modify the state of the store (visible output).

: the component can dispatch actions with mutations that will modify the state of the store (visible output). Route: The component can interact with the router generating a new route that will be rendered by a new Page Component.

Ideally, I would always try to test my components by just modifying the previous inputs and asserting changes in the outputs. That way our tests would be more independent of it’s implementation and, therefore, less brittle. However, in this post we will see that, to test certain features, we won’t be able to avoid accessing the implementation of the component under test.

Setup

I assume that you already know how to install and setup jest and vue-test-utils.

I added this line to package.json:

"test": "jest -c jest.config.js"

, so that I can run the tests with npm test (you can check the jest setup in jest.config.js).

Creating Fake Data

The HackerNews clone defines an API that fetches data from a firebase database but, we are not going to test the internals of that API in the unit tests of a component. Instead, we are going to stub that API.

How do we get the fake data for the stub?

I simply executed the app and saved the real responses, storing them as a JSON file. That way I could create this fake data file that exports constants that we will use in our tests, such as:

A list of 60 hacker news

A list of id’s of all the news

A fake Item

A fake User

Stubbing the backend API

Once we have proper fake data, we need to know how to create and use an API stub that replaces the real implementation in our tests.

The real api uses a firebase client/server and a cache underneath but we are not interested in those details in the scope of our component unit tests. All we need to do is understanding API’s public contract and creating a fake-api that fulfils that contract.

For example, the real api has a fetchItem function that receives an id and queries the database/cache, returning a Promise with an Item. We can simply replace that complex function with our fake implementation that returns a Promise with a fakeUser:

export function fetchItem (id) {

return Promise.resolve(fakeItemList[id])

}

Injecting the fake api in our tests

Once we have faked every API method, we need to decide how to replace the real implementation by the fake one in our tests.

We could try to change the design of the existing app to be able to inject dependencies explicitly but we wanted to leave the original production code as it is, so we will need to use some black magic to replace the whole API module in our test.

One of the common ways is using Jest manual mocks but, in order to simplify the explanations, I will exceptionally use a dirtier trick😅:

//jest.config.js ... "moduleNameMapper": {

"^@/(.*)$": "<rootDir>/src/$1",

'../api': '<rootDir>/src/api/__mocks__/fake-api.js'

}, ...

with the previous setup, every unit test will use fake-api.js as the default API instead of using the production one 🧙‍✨

Where do I begin to test my app?

As a TDD practitioner, I prefer to write my tests before the implementation but, in this case, we want to test an existing “legacy code” so first step is to understand its main pieces. Vue HackerNews clone has 3 main pages:

ListView : there are 5 list routes (top, new, show, ask, job) that have the exact same structure

: there are 5 list routes (top, new, show, ask, job) that have the exact same structure ItemView : this page is opened when you click one item in a list

: this page is opened when you click one item in a list UserView: this page is opened when you click an author link

Each one of the previous pages is a root component that renders the page when we visit its route. I’ll call them them Page components but you can also call them Root, Container or View components.

Example of ListView Page component showing the Top news

I always like to test the behaviour of my applications from its upper layer (the Page components) and I always try to avoid mocking the children components as much as possible. The benefits are:

The intention of your tests will be easier to understand

You will not need to change your tests when the inner structure/implementation changes but your behaviour does not

It will lead you to better understand the relationships between your inner components and their interactions with the external world

Let’s test our first page component to understand what I mean.

Our First Test

We will start with a very simple page component, UserView.vue, that is opened by this route:

{ path: '/user/:id', component: UserView }

This component receives the :id of a user through the router params and renders it with a computed property that takes the user from the Vuex store:

computed: {

user () {

return this.$store.state.users[this.$route.params.id]

}

},

This is an example of a rendered UserView:

Example of rendered UserView.vue page

Identifying Inputs and Outputs of UserView component

As we saw, it’s very important to identify inputs and outputs to understand the “contract” of the component that we want to test.

Inputs of UserView.vue component?

Props : this component does not have props

: this component does not have props Route : we pass the user id as a route param

: we pass the as a route param DOM : UserView is a simple component and we won’t interact with the DOM in our tests.

: UserView is a simple component and we won’t interact with the DOM in our tests. VUEX store: The component needs a user from store.state.users.

Outputs of UserView.vue component?

DOM: Obviously, our component will render its UI.

Obviously, our component will render its UI. VUEX store : the component dispatches FETCH_USER action when it is loaded.

: the component dispatches FETCH_USER action when it is loaded. Route: Our component does not generate new routes.

Setting up the Vuex Store:

We need a clean instance of a Vuex store to test our components. We could create a localVue instance with a new Vuex object but our project already uses the createStore() function to initialise the store and we will simply call it from each test to have a new clean store.

Mocking routes:

Our UserView.vue component is expecting a route with an :id param. That’s why we created a builder function to create a fake route from an id:

const userRoute = (id) => ({

path: '/user',

params: { id }

})

And we will use vue-test-utils mocks to pass that route to the component under test:

// route will contain a route created with userRoute() builder

const wrapper = mount(UserView, { store, router,

mocks: {

$route: route

}

})

Our first assertion

We finally have everything that we need to code our first UserView test:

where we tested that our component renders HTML containing an h1 title with the id of our fake user ✅

Testing Nuxt asynchronous load

In our previous test, we initialised our store.state.users with a fake User but, in the the real world, this user would be fetched from the API by performing an asynchronous request so, we need to improve our tests if we want it to be as real as possible.

The project is using Nuxt for Server Side rendering and it uses a special hook method called asyncData():

asyncData ({ store, route: { params: { id }}}) {

return store.dispatch('FETCH_USER', { id })

},

How do we test it?

Ideally, we’ll always try to avoid accessing the internals of our components to avoid coupling our tests to the implementation but, in this case, it’s the only alternative that we have, so we will use the vm property to call our asyncData method and execute a our improved version of the test 💪

We will also need to wait for Vue to asynchronously re-render the DOM so we will use a resolvePromises function that will help us with that. Check this fantastic article if you are interested in the details.

wrapper.vm.$options.asyncData({ store, route })

await resolvePromises()

Finally here we have the new asynchronous test 🚀:

Testing filters

Now that we wrote our first test, we want more. For example, checking that the component renders the days, hours or minutes since the creation of the user. The app uses a timeAgo Vue Filter for that:

{{ user.created | timeAgo }} ago

and all the Vue filters are registered in app.js with the following code:

Object.keys(filters).forEach(key => {

Vue.filter(key, filters[key])

})

How do we test these globally registered filters?

We are not calling app.js in our test but, jest allows us to create a setup file (check out our jest-setup.js) where we can initialise global things like filters so that we will be able to use them in the scope of our tests.

Our filter test will look like this:

Where, knowing that our fake user was created the 7th of September, we will use Jest to stub Date.now and make the test independent of real time®:

Date.now = jest.fn(() => nineOfSeptember)

Testing dispatched actions

We identified Vuex actions as one of the possible outputs (or side effects) of our components and sometimes we will be implicitly testing them.

For example, when we called asyncData hook in UserView test we where executing this code:

asyncData ({ store, route: { params: { id }}}) {

return store.dispatch('FETCH_USER', { id })

},

so we were implicitly testing that FETCH_USER action was correctly dispatched.

Do we need to also tests it explicitly? It depends, but as our applications grow, we will tend to have more complex actions depending on more external services, receiving more complex payloads that will be more difficult to understand, so I find a good idea to test the interactions of our components with the Vuex store or even stubbing the dispatch method in the scope of our component’s unit tests and testing the implementation of the actions in another kind of integration tests.

Here is an example of how easy is to check that our UserView.vue dispatches an action (using jest.spyOn to verify the interactions):

Testing more components

Following the previous tips, we are ready to test the rest of our Page components. For example, here we have the tests for our ItemView.vue component:

Or we can even test more sophisticated things. For example, what happens when some Item is added to a list in real time:

Here is the repo where you can check all the details around this demo tests.

Future steps

We have seen some examples of the most important tests in our test pyramid but I wouldn’t finish here.

Inner components

As we saw, I like to test my apps from the upper layers where I try to cover the most relevant cases that I need to check my features. But I don’t try to cover all the possible cases from the outside, for that, it’s also important to test the inner components where you can easily test all the edge cases. For example, getters, filters or presentational components are very easy to test with really simple isolated tests.

Complex DOM interactions

As our real apps grow we will find more challenges and we will need to improve our current tests. For example, HackerNews app is a read only app where the user cannot create content but, in bigger apps, you normally have complex forms where the user can interact with the DOM to generate state changes and side effects in many sophisticated ways. In that case, I always use the Page Object pattern to reduce the coupling of our unit tests to the html/css implementation.

Testing the implementation of the backend API

In this post we saw how to mock the backend API forget about it’s implementation in the unit tests of our components but, I would also unit test that implementation (mainly the caching), for example, by using a firebase stub (we could stub it manually or using tools like firebase-mock).

I would also add some contract tests to test that the integration with a real firebase instance works.

Conclusion

Unit testing real VueJs apps can be challenging because we need to know about good design and testing practices, we need to control our tools (jest, vue-test-utils…) and we also need to be creative to overcome certain blockers.

Having tools like Cypress for end to end testing is a blessing but IMHO we can never forget about unit tests because they are essential to improve the design of our apps, the knowledge of our libraries/frameworks and the joy of coding. Long live unit tests!