In this chapter, we will look at approaches to unit testing Vue.js applications in order to improve our quality and speed of delivery. We will also look at using tests to drive development using Test-Driven Development (TDD).
As we proceed, you will gain an understanding of why code needs to be tested and what kinds of testing can be employed on different parts of a Vue.js application. You will see how to unit test isolated components and their methods using shallow rendering and vue-test-utils, and you will learn how to test asynchronous component code. Throughout the course of the chapter, you will gain familiarity with techniques to write efficient unit tests for mixins and filters. Toward the end of the chapter, you will become familiar with approaches to testing a Vue.js applications that includes routing and Vuex, and you will learn about using snapshot tests to validate your user interface.
In this chapter, we will look at the purpose and approaches to testing Vue.js applications effectively.
In previous chapters, we saw how to build reasonably complex Vue.js applications. This chapter is about testing them to maintain code quality and prevent defects.
Unit testing will allow us to write fast and specific tests that we can develop against and ensure that features don't exhibit unwanted behavior. We'll see how to write unit tests for different parts of a Vue.js application, such as components, mixins, filters, and routing. We will use tools supported by the Vue.js core team, such as vue-test-utils, and tools supported by the rest of the open source community, such as the Vue Testing library and the Jest testing framework. These different tools will serve to illustrate different philosophies and approaches to unit testing.
Testing is crucial for ensuring that the code does what it's meant to do.
Quality production software is empirically correct. That means that for the enumerated cases that developers and testers have found, the application behaves as expected.
This lies in contrast with software that has proven to be correct, which is a very time-consuming endeavor and is usually part of academic research projects. We are still at the point where correct software (proven) is still being built to show what kinds of systems are possible to build with this constraint of correctness.
Testing prevents the introduction of defects such as bugs and regressions (that is, when a feature stops working as expected). In the next section, we will learn about the various types of testing.
The testing spectrum spans from end-to-end testing (by manipulating the user interface) to integration tests, and finally to unit tests. End-to-end tests test everything, including the user interface, the underlying HTTP services, and even database interactions; nothing is mocked. For example, if you've got an e-commerce application, an end-to-end test might actually place a real order with a real credit card, or it might place a test order, with a test credit card.
End-to-end tests are costly to run and maintain. They require the use of full-blown browsers controlled through programmatic drivers such as Selenium, WebdriverIO, or Cypress. This type of test platform is costly to run, and small changes in the application code can cause end-to-end tests to start failing.
Integration or system-level tests ensure that a set of systems is working as expected. This will usually involve deciding on a limit as to where the system under test lies and allowing it to run, usually against mocked or stubbed upstream services and systems (which are therefore not under test). Since external data access is stubbed, a whole host of issues, such as timeouts and flakes, can be reduced (when compared to end-to-end tests). Integration test suites are usually fast enough to run as a continuous integration step, but the full test suite tends not to be run locally by engineers.
Unit tests are great at providing fast feedback during development. Unit testing paired with TDD is part of extreme programming practice. Unit tests are great at testing complicated logic or building a system from its expected output. Unit tests are usually fast enough to run that developers code against them before sending their code for review and continuous integration tests.
The following is an interpretation of the pyramid of testing. It can be interpreted as: you should have a high number of cheap and fast unit tests, a reasonable number of system tests, and a few end-to-end UI tests:
Now that we've looked at why we should be testing applications, let's start writing some tests.
To illustrate how quick and easy it is to get started with automated unit tests in a Vue CLI project, we will start by setting up and writing a unit test with Jest, @vue-test-utils. There is an official Vue CLI package that can be used to generate a setup that includes unit testing with Jest and vue-test-utils. The following command should be run in a project that has been set up with Vue CLI:
vue add @vue/unit-jest
Vue CLI adds Jest as the test runner, @vue/test-utils, the official Vue.js testing utilities, and vue-jest, a processor for .vue single-file component files in Jest. It adds a test:unit script.
By default, it creates a tests/unit folder, which we'll remove. Instead, we can create a __tests__ folder and create an App.test.js file as follows.
We will use shallowMount to render the application and test that it displays the correct text. For the purposes of this example, we'll use the text: "The Vue.js Workshop Blog".
shallowMount does a shallow render, which means that only the top level of a component is rendered; all the child components are stubbed. This is useful for testing a component in isolation since the child components' implementations are not run:
import { shallowMount } from '@vue/test-utils'
import App from '../src/App.vue'
test('App renders blog title correctly', () => {
const wrapper = shallowMount(App)
expect(wrapper.text()).toMatch("The Vue.js Workshop Blog")
})
This test will fail when we run npm run test:unit because we do not have The Vue.js Workshop Blog in the App component:
In order to get the test to pass, we can implement our blog title heading in the App.vue file:
<template>
<div id="app" class="p-10">
<div class="flex flex-col">
<h2
class="leading-loose pb-4 flex justify-center m-auto md:w-1/3 text-xl mb-8 font-bold text-gray-800 border-b"
>
The Vue.js Workshop Blog
</h2>
</div>
</div>
</template>
Now that we have got the right heading, npm run test:unit will pass:
We can also check that it renders as expected in the browser:
The Vue.js Workshop Blog
You have just completed your first piece of TDD. This process started by writing a test that failed. This failure was followed by an update to the code under test (in this case the App.vue component), which made the failing test pass. The TDD process gives us confidence that our features have been tested properly since we can see that tests fail before they pass when we update the code that drives our feature.
Components are at the core of Vue.js applications. Writing unit tests for them is straightforward with vue-test-utils and Jest. Having tests that exercise the majority of your components gives you confidence that they behave as designed. Ideal unit tests for components run quickly and are simple.
We'll carry on building the blog application example. We have now built the heading, but a blog usually also needs a list of posts to display.
We'll create a PostList component. For now, it will just render a div wrapper and support a posts Array prop:
<template>
<div class="flex flex-col w-full">
</div>
</template>
<script>
export default {
props: {
posts: {
type: Array,
default: () => []
}
}
}
</script>
We can add some data in the App component:
<script>
export default {
data() {
return {
posts: [
{
title: 'Vue.js for React developers',
description: 'React has massive popularity here are the key benefits of Vue.js over it.',
tags: ['vue', 'react'],
},
{
title: 'Migrating an AngularJS app to Vue.js',
description: 'With many breaking changes, AngularJS developers have found it easier to retrain to Vue.js than Angular 2',
tags: ['vue', 'angularjs']
}
]
}
}
}
</script>
Now that we have some posts, we can pass them as a bound prop to the PostList component from the App component:
<template>
<!-- rest of template -->
<PostList :posts="posts" />
<!-- rest of template -->
</template>
<script>
import PostList from './components/PostList.vue'
export default {
components: {
PostList
},
// rest of component properties
}
Our PostList component will render out each post in a PostListItem component, which we'll create as follows.
PostListItem takes two props: title (which is a string) and description (also a string). It renders them in an h3 tag and a p tag, respectively:
<template>
<div class="flex flex-col m-auto w-full md:w-3/5 lg:w-2/5 mb-4">
<h3 class="flex text-md font-semibold text-gray-700">
{{ title }}</h3>
<p class="flex leading-relaxed">{{ description }}</p>
</div>
</template>
<script>
export default {
props: {
title: {
type: String
},
description: {
type: String
}
}
}
</script>
We now need to loop through the posts and render out a PostListItem component with relevant props bound in the PostList.vue component:
<template>
!-- rest of template -->
<PostListItem
v-for="post in posts"
:key="post.slug"
:title="post.title"
:description="post.description"
/>
<!-- rest of template -->
</template>
<script>
import PostListItem from './PostListItem.vue'
export default {
components: {
PostListItem,
},
// rest of component properties
}
</script>
We can now see the heading and the post list in the application:
The Vue.js Workshop blog
To test the PostListItem component, we can shallow render with some arbitrary title and description props set, and check that they get rendered:
import { shallowMount } from '@vue/test-utils'
import PostListItem from '../src/components/PostListItem.vue'
test('PostListItem renders title and description correctly', () => {
const wrapper = shallowMount(PostListItem, {
propsData: {
title: 'Blog post title',
description: 'Blog post description'
}
})
expect(wrapper.text()).toMatch("Blog post title")
expect(wrapper.text()).toMatch("Blog post description")
})
The test output of npm run test:unit __tests__/PostListItem.test.js is as follows; the component passes the test:
Next, we'll see one of the pitfalls of shallow rendering. When testing the PostList component, all we can do is test the number of PostListItem components it's rendering:
import { shallowMount } from '@vue/test-utils'
import PostList from '../src/components/PostList.vue'
import PostListItem from '../src/components/PostListItem.vue'
test('PostList renders the right number of PostListItem', () => {
const wrapper = shallowMount(PostList, {
propsData: {
posts: [
{
title: "Blog post title",
description: "Blog post description"
}
]
}
})
expect(wrapper.findAll(PostListItem)).toHaveLength(1)
})
This passes, but we are testing something that the user will not directly interact with, the number of PostListItem instances rendered in PostList, as shown in the following screenshot:
A better solution is to use the mount function, which renders the full component tree, whereas the shallow function would only render out the children of the component being rendered. With mount, we can assert that the titles and descriptions are rendered to the page.
The drawback of this approach is that we're testing both the PostList component and the PostListItem component since the PostList component doesn't render the title or description; it renders a set of PostListItem components that in turn render the relevant title and description.
The code will be as follows:
import { shallowMount, mount } from '@vue/test-utils'
import PostList from '../src/components/PostList.vue'
// other imports and tests
test('PostList renders passed title and description for each passed post', () => {
const wrapper = mount(PostList, {
propsData: {
posts: [
{
title: 'Title 1',
description: 'Description 1'
},
{
title: 'Title 2',
description: 'Description 2'
}
]
}
})
const outputText = wrapper.text()
expect(outputText).toContain('Title 1')
expect(outputText).toContain('Description 1')
expect(outputText).toContain('Title 2')
expect(outputText).toContain('Description 2')
})
The new tests pass as per the following output of npm run test:unit __tests__/PostList.vue:
We have now seen how to write unit tests for Vue.js components using Jest and vue-test-utils. These tests can be run often, and test runs complete within seconds, which gives us near-immediate feedback while working on new or existing components.
When creating the fixture for posts, we populated a tags field with vue, angularjs, and react but did not display them. To make tags useful, we will display the tags in the post list.
To access the code files for this exercise, refer to https://packt.live/2HiTFQ1:
// rest of tests and imports
test('PostListItem renders tags with a # prepended to them', () => {
const wrapper = shallowMount(PostListItem, {
propsData: {
tags: ['react', 'vue']
}
})
expect(wrapper.text()).toMatch('#react')
expect(wrapper.text()).toMatch('#vue')
})
This test fails when run with npm run test:unit __tests__/PostListItem.test.js:
<template>
<!-- rest of template -->
<div class="flex flex-row flex-wrap mt-4">
<a
v-for="tag in tags"
:key="tag"
class="flex text-xs font-semibold px-2 py-1 mr-2 rounded border border-blue-500 text-blue-500"
>
#{{ tag }}
</a>
</div>
<!-- rest of template -->
</template>
<script>
export default {
props: {
// rest of props
tags: {
type: Array,
default: () => []
}
}
}
</script>
With the PostListItem component implemented, the unit test should now pass:
However, the tags are not displayed in the application:
// rest of tests and imports
test('PostList renders tags for each post', () => {
const wrapper = mount(PostList, {
propsData: {
posts: [
{
tags: ['react', 'vue']
},
{
tags: ['html', 'angularjs']
}
]
}
})
const outputText = wrapper.text()
expect(outputText).toContain('#react')
expect(outputText).toContain('#vue')
expect(outputText).toContain('#html')
expect(outputText).toContain('#angularjs')
})
As per our application output, the test is failing when run with npm run test:unit __tests__/PostList.test.js:
<template>
<!-- rest of template-->
<PostListItem
v-for="post in posts"
:key="post.slug"
:title="post.title"
:description="post.description"
:tags="post.tags"
/>
<!-- rest of template -->
</template>
The failing unit test now passes, as shown in the following screenshot:
The tags also appear in the application, as shown in the following screenshot:
We have now seen how we can test rendered component output with both the shallow rendering and mounting of components. Let's briefly understand what each of these terms means:
Next, we'll look at how to test component methods.
Since filters and mixins generate their output based solely on function parameters, they are straightforward to unit test. It is not recommended to test methods unless it's strictly necessary since the user doesn't call methods on the component directly. The users see the rendered UI, and their interactions with the application are manifested as events (for example, click, input change, focus change, and scroll).
For example, a filter that truncates its input to eight characters would be implemented as follows:
<script>
export default {
filters: {
truncate(value) {
return value && value.slice(0, 8)
}
}
}
</script>
There are two options to test it. We could test it directly by importing the component and calling truncate on some input, as per the truncate.test.js file:
import PostListItem from '../src/components/PostListItem.vue'
test('truncate should take only the first 8 characters', () => {
expect(
PostListItem.filters.truncate('longer than 8 characters')
).toEqual('longer t')
})
The alternative is to check where it's being used in the PostListItem component:
<template>
<!-- rest of template -->
<h3 class="flex text-md font-semibold text-gray-700">
{{ title | truncate }}
</h3>
<!-- rest of template -->
</template>
Now we can test truncate by checking what happens when we pass a long title into the PostListItem component in the PostListItem.test.js file, which we do in the following test:
// imports
test('PostListItem renders title and description correctly', () => {
const wrapper = shallowMount(PostListItem, {
propsData: {
title: 'Blog post title',
description: 'Blog post description'
}
})
expect(wrapper.text()).toMatch("Blog post title")
expect(wrapper.text()).toMatch("Blog post description")
})
// other tests
The preceding code will generate the output shown in the following screenshot:
To fix this, we could update the failing test to expect Blog pos instead of Blog post title.
These two approaches are great for testing filters. As we saw before with mount versus shallow rendering, the difference is in the tightness of the unit test. The tighter unit test is the direct filters.truncate() test since it directly accesses the truncate filter. The looser unit test is the test using passed props and validating the component output. A tighter unit will usually mean tests are simpler, but it comes at the cost of sometimes testing functionality in a fashion that is very removed from how the end user perceives it. For example, the user would never call filters.truncate() directly.
We have seen how to test an arbitrary truncate filter. We will now implement an ellipsis filter and test it.
The ellipsis filter will be applied to the post description and will limit its length to 40 characters plus ….
We have seen how to test an arbitrary truncate filter; we will now implement an ellipsis filter and test it.
To access the code files for this exercise, refer to https://packt.live/2UK9Mcs.
Now let's look at the steps to build and test an ellipsis filter:
import PostListItem from '../src/components/PostListItem.vue'
test('ellipsis should do nothing if value is less than 50 characters', () => {
expect(
PostListItem.filters.ellipsis('Less than 50 characters')
).toEqual('Less than 50 characters')
})
test('ellipsis should truncate to 50 and append "..." when longer than 50 characters', () => {
expect(
PostListItem.filters.ellipsis(
'Should be more than the 50 allowed characters by a small amount'
)
).toEqual('Should be more than the 50 allowed characters by a...')
})
<script>
export default {
// rest of component properties
filters: {
ellipsis(value) {
return value && value.length > 50
? `${value.slice(0, 50)}...`
: value
}
}
}
</script>
In this case, the test now passes npm run test:unit __tests__/ellipsis.test.js, as shown in Figure 12.14:
// other tests and imports
test('PostListItem truncates long descriptions', () => {
const wrapper = shallowMount(PostListItem, {
propsData: {
description: 'Very long blog post description that goes over 50 characters'
}
})
expect(wrapper.text()).toMatch("Very long blog post description that goes over 50 ...")
})
This test fails since we don't use the filter in the component template. The output will be as follows:
<template>
<!-- rest of template -->
<p class="flex leading-relaxed">{{ description | ellipsis }} </p>
<!-- rest of template -->
</template>
Now, the test will pass, as displayed in the following screenshot:
We can see the descriptions being truncated in the application interface in the browser, as follows:
We have now seen how to test filters and other properties of a Vue.js component not only by testing directly against the object but also by testing the functionality where it is being used in component-level tests.
Next, we will see how to deal with an application that uses Vue.js routing.
We have currently got an application that renders what is our blog home page or feed view.
Next, we should have post pages. To do this, we will use Vue Router, as covered in previous chapters, and ensure that our routing works as designed with unit tests.
Vue Router is installed using npm, specifically, npm install vue-router, and wiring it up in the main.js file:
// other imports
import router from './router'
// other imports and configuration
new Vue({
render: h => h(App),
router,
}).$mount(‹#app›)
The router.js file registers vue-router with Vue using Vue.use and instantiates a VueRouter instance:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
export default new VueRouter({})
A router with no routes isn't very useful. We'll define the root path (/) to display the PostList component in the router.js file, as follows:
// other imports
import PostList from './components/PostList.vue'
// registering of Vue router
const routes = [
{
path: '/',
component: PostList
}
]
export default new VueRouter({
routes
})
Now that we've got our initial route, we should update the App.vue file to leverage the component being rendered by the router. We'll render render-view instead of directly using PostList. The posts binding, however, stays the same:
<template>
<!-- rest of template -->
<router-view
:posts="posts"
/>
<!-- rest of template -->
</template>
Now, our posts in the App.vue file are missing a bit of core data to render a SinglePost component. We need to make sure to have the slug and content properties to render something useful on our SinglePost page:
<script>
export default {
data() {
return {
posts: [
{
slug: 'vue-react',
title: 'Vue.js for React developers',
description: 'React has massive popularity here are the key benefits of Vue.js over it.',
content:
'React has massive popularity here are the key benefits of Vue.js over it.
See the following table, we'll also look at how the is the content of the post.
There's more, we can map React concepts to Vue and vice-versa.',
tags: ['vue', 'react'],
},
{
slug: 'vue-angularjs',
title: 'Migrating an AngularJS app to Vue.js',
description: 'With many breaking changes, AngularJS developers have found it easier to retrain to Vue.js than Angular 2',
content:
'With many breaking changes, AngularJS developers have found it easier to retrain to Vue.js than Angular 2
Vue.js keeps the directive-driven templating style while adding a component model.
It's performant thanks to a great reactivity engine.',
tags: ['vue', 'angularjs']
}
]
}
}
}
</script>
We can now start working on a SinglePost component. For now, we'll just have some placeholders in the template. Also, SinglePost will receive posts as a prop, so we can fill that in as well:
<template>
<div class="flex flex-col w-full md:w-1/2 m-auto">
<h2
class="font-semibold text-sm mb-4"
>
Post: RENDER ME
</h2>
<p>Placeholder for post.content</p>
</div>
</template>
<script>
export default {
props: {
posts: {
type: Array,
default: () => []
}
}
}
</script>
Next, we will register SinglePost in router.js, with the /:postId path (which will be available to the component under this.$route.params.postId):
// other imports
import SinglePost from './components/SinglePost.vue'
// vue router registration
const routes = [
// other route
{
path: '/:postId',
component: SinglePost
}
]
// exports and router instantiation
If we switch back to implementing the SinglePost component, we've got access to postId, which will map to the slug in the posts array, and we've also got access to posts since it's being bound onto render-view by App. Now we can create a computed property, post, which finds posts based on postId:
<script>
export default {
// other properties
computed: {
post() {
const { postId } = this.$route.params
return posts.find(p => p.slug === postId)
}
}
}
</script>
From this computed post property, we can extract title and content if post exists (we have to watch out for posts that don't exist). So, still in SinglePost, we can add the following computed properties:
<script>
export default {
// other properties
computed: {
// other computed properties
title() {
return this.post && this.post.title
},
content() {
return this.post && this.post.content
}
}
}
</script>
We can then replace the placeholders in the template with the value of the computed properties. So, our template ends up as follows:
<template>
<div class="flex flex-col w-full md:w-1/2 m-auto">
<h2
class="font-semibold text-sm mb-4"
>
Post: {{ title }}
</h2>
<p>{{ content }}</p>
</div>
</template>
Finally, we should make the whole post item a router-link that points to the right slug in the PostListItem.vue file:
<template>
<router-link
class="flex flex-col m-auto w-full md:w-3/5 lg:w-2/5 mb-4"
:to="`/${slug}`"
>
<!-- rest of the template -->
</router-link>
</template>
router-link is a Vue Router-specific link, which means that on the PostList page, upon clicking on a post list item, we are taken to the correct post's URL, as shown in the following screenshot:
We'll be redirected to the correct URL, the post's slug, which will render the right post by slug, as shown in Figure 12.19.
To test vue-router, we will explore a new library that's better suited to testing applications with routing and a Vuex store, the Vue Testing library, which is accessible on npm as @testing-library/vue.
We can install it with npm install --save-dev @testing-library/vue.
To test SinglePost routing and rendering, we do the following. First of all, we should be able to access the SinglePost view by clicking on a post title in the PostList view. In order to do this, we check that we're on the home page by examining the content (we'll see two posts with the titles). Then we'll click a post title and check that the content from the home page is gone and the post content is displayed:
import {render, fireEvent} from '@testing-library/vue'
import App from '../src/App.vue'
import router from '../src/router.js'
test('Router renders single post page when clicking a post title', async () => {
const {getByText, queryByText} = render(App, { router })
expect(queryByText('The Vue.js Workshop Blog')).toBeTruthy()
expect(queryByText('Vue.js for React developers')).toBeTruthy()
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeTruthy()
await fireEvent.click(getByText('Vue.js for React developers'))
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeFalsy()
expect(queryByText('Post: Vue.js for React developers')). toBeTruthy()
expect(
queryByText(
'React has massive popularity here are the key benefits of Vue.js over it. See the following table, we'll also look at how the is the content of the post. There's more, we can map React concepts to Vue and vice-versa.'
)
).toBeTruthy()
})
We should check that navigating directly to a valid post URL will yield the correct result. In order to do this, we'll use router.replace('/') to clear any state that's set, and then use router.push() with a post slug. We will then use the assertions from the previous code snippet to validate that we are on the SinglePost page, not the home page:
test('Router renders single post page when a slug is set', async () => {
const {queryByText} = render(App, { router })
await router.replace('/')
await router.push('/vue-react')
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeFalsy()
expect(queryByText('Post: Vue.js for React developers')). toBeTruthy()
expect(
queryByText(
'React has massive popularity here are the key benefits of Vue.js over it. See the following table, we'll also look at how the is the content of the post. There's more, we can map React concepts to Vue and vice-versa.'
)
).toBeTruthy()
})
Those two tests work as expected when run with npm run test:unit __tests__/SinglePost.test.js. The following screenshot displays the desired output:
We have now seen how to use the Vue.js Testing library to test an application that uses vue-router.
Much like we have built a single-post page, we'll now build a tag page, which is similar to the PostList component except only posts with a certain tag are displayed and each post is a link to a relevant single-post view.
To access the code files for this exercise, refer to https://packt.live/39cJqZd:
<template>
<div class="flex flex-col md:w-1/2 m-auto">
<h3
class="font-semibold text-sm text-center mb-6"
>
#INSERT_TAG_NAME
</h3>
<PostList :posts="[]" />
</div>
</template>
<script>
import PostList from './PostList'
export default {
components: {
PostList
},
props: {
posts: {
type: Array,
default: () => []
}
},
}
</script>
// other imports
import TagPage from './components/TagPage.vue'
// Vue router registration
const routes = [
// other routes
{
path: '/tags/:tagName',
component: TagPage
}
]
// router instantiation and export
<script>
// imports
export default {
// rest of component
computed: {
tagName() {
return this.$route.params.tagName
},
tagPosts() {
return this.posts.filter(p => p.tags.includes(this.tagName))
}
}
}
</script>
<template>
<div class="flex flex-col md:w-1/2 m-auto">
<h3
class="font-semibold text-sm text-center mb-6"
>
#{{ tagName }}
</h3>
<PostList :posts="tagPosts" />
</div>
</template>
Now, the page displays as follows if we navigate, for example, to /tags/angularjs:
<template>
<!-- rest of template -->
<router-link
:to="`/tags/${tag}`"
v-for="tag in tags"
:key="tag"
class="flex text-xs font-semibold px-2 py-1 mr-2 rounded border border-blue-500 text-blue-500"
>
#{{ tag }}
</router-link>
<!-- rest of template -->
</template>
import {render, fireEvent} from '@testing-library/vue'
import App from '../src/App.vue'
import router from '../src/router.js'
test('Router renders tag page when clicking a tag in the post list item', async () => {
const {getByText, queryByText} = render(App, { router })
expect(queryByText('The Vue.js Workshop Blog')). toBeTruthy()
expect(queryByText('Vue.js for React developers')). toBeTruthy()
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeTruthy()
await fireEvent.click(getByText('#angularjs'))
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeTruthy()
expect(queryByText('Vue.js for React developers')).toBeFalsy()
expect(queryByText('React')).toBeFalsy()
})
// import & other tests
test('Router renders tag page when a URL is set', async () => {
const {queryByText} = render(App, { router })
await router.push('/')
await router.replace('/tags/angularjs')
expect(queryByText('Migrating an AngularJS app to Vue.js')). toBeTruthy()
expect(queryByText('Vue.js for React developers')). toBeFalsy()
expect(queryByText('React')).toBeFalsy()
})
The tests pass since the application is working as expected. Therefore, the output will be as follows:
We've now seen how to implement and test an application that includes vue-router. In the next section, we will learn about testing Vuex in detail.
To show how to test a component that relies on Vuex (Vue.js's official global state management solution), we'll implement and test a newsletter subscription banner.
To start with, we should create the banner template. The banner will contain a Subscribe to the newsletter call to action and a close icon:
<template>
<div class="text-center py-4 md:px-4">
<div
class="py-2 px-4 bg-indigo-800 items-center text-indigo-100
leading-none md:rounded-full flex md:inline-flex"
role="alert"
>
<span
class="font-semibold ml-2 md:mr-2 text-left flex-auto"
>
Subscribe to the newsletter
</span>
<svg
class="fill-current h-6 w-6 text-indigo-500"
role="button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<title>Close</title>
<path
d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651
3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1. 2
1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1
1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1. 698z"
/>
</svg>
</div>
</div>
</template>
We can display the NewsletterBanner component in the App.vue file as follows:
<template>
<!-- rest of template -->
<NewsletterBanner />
<!-- rest of template -->
</template>
<script>
import NewsletterBanner from './components/NewsletterBanner.vue'
export default {
components: {
NewsletterBanner
},
// other component properties
}
</script>
We'll then install Vuex with the npm install --save vuex command. Once Vuex is installed, we can initialize our store in a store.js file as follows:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {}
})
Our Vuex store is also registered in the main.js file:
// other imports
import store from './store'
// other configuration
new Vue({
// other vue options
store
}).$mount('#app')
In order to decide whether the newsletter banner should be displayed or not, we need to add an initial state to our store:
// imports and configuration
export default new Vuex.Store({
state: {
dismissedSubscribeBanner: false
}
})
To close the banner, we need a mutation that will set dismissedSubscribeBanner to true:
// imports and configuration
export default new Vuex.Store({
// other store configuration
mutations: {
dismissSubscribeBanner(state) {
state.dismissedSubscribeBanner = true
}
}
})
We can now use the store state and the dismissSubscribeBanner mutation to decide whether to show the banner (using v-if) and whether to close it (binding to a click on the close button):
<template>
<div v-if="showBanner" class="text-center py-4 md:px-4">
<!-- rest of template -->
<svg
@click="closeBanner()"
class="fill-current h-6 w-6 text-indigo-500"
role="button"
xmlns=http://www.w3.org/2000/svg
viewBox="0 0 20 20"
>
<!-- rest of the template -->
</div>
</template>
<script>
export default {
methods: {
closeBanner() {
this.$store.commit('dismissSubscribeBanner')
}
},
computed: {
showBanner() {
return !this.$store.state.dismissedSubscribeBanner
}
}
}
</script>
At this point, the banner looks like this in a browser:
To write unit tests, we will use the Vue Testing library, which provides a facility for injecting a Vuex store. We'll need to import the store and the NewsletterBanner component.
We can start with a sanity check that, by default, the newsletter banner is displayed:
import {render, fireEvent} from '@testing-library/vue'
import NewsletterBanner from '../src/components/ NewsletterBanner.vue'
import store from '../src/store'
test('Newsletter Banner should display if store is initialised with it not dismissed', () => {
const {queryByText} = render(NewsletterBanner, { store })
expect(queryByText('Subscribe to the newsletter')).toBeTruthy()
})
The next check should be that if the store has dismissedSubscribeBanner: true, the banner should not be displayed:
// imports and other tests
test('Newsletter Banner should not display if store is initialised with it dismissed', () => {
const {queryByText} = render(NewsletterBanner, { store: {
state: {
dismissedSubscribeBanner: true
}
} })
expect(queryByText('Subscribe to the newsletter')).toBeFalsy()
})
The final test we'll write is to make sure that clicking the banner's close button commits a mutation to the store. We can do this by injecting a stub as the dismissSubscribeBanner mutation and checking that it is called when clicking the close button:
// imports and other tests
test('Newsletter Banner should hide on "close" button click', async () => {
const dismissSubscribeBanner = jest.fn()
const {getByText} = render(NewsletterBanner, {
store: {
...store,
mutations: {
dismissSubscribeBanner
}
}
})
await fireEvent.click(getByText('Close'))
expect(dismissSubscribeBanner).toHaveBeenCalledTimes(1)
})
The tests will now pass when run with npm run test:unit __tests__/NewsletterBanner.test.js, as follows:
We've now seen how the Vue.js Testing library can be used to test application functionality driven by Vuex.
We'll now look at how to implement a cookie disclaimer banner using Vuex and how to test it with the Vue.js Testing library.
We will store whether the cookie banner is showing in Vuex (the default is true); when the banner is closed, we will store it in Vuex.
Test this opening/closing with a mock Vuex store. To access the code files for this exercise, refer to https://packt.live/36UzksP:
<template>
<div
class="flex flex-row bg-green-100 border text-center border-green-400
text-green-700 mt-8 px-4 md:px-8 py-3 rounded relative"
role="alert"
>
<div class="flex flex-col">
<strong class="font-bold w-full flex">Cookies Disclaimer
</strong>
<span class="block sm:inline">We use cookies to improve your experience</span>
</div>
<button
class="ml-auto align-center bg-transparent hover:bg-green-500
text-green-700 font-semibold font-sm hover:text-white py-2 px-4 border
border-green-500 hover:border-transparent rounded"
>
I agree
</button>
</div>
</template>
<template>
<!-- rest of template -->
<CookieBanner />
<!-- rest of template -->
</template>
<script>
// other imports
import CookieBanner from './components/CookieBanner.vue'
export default {
components: {
// other components
CookieBanner
},
// other component properties
}
</script>
// imports and configuration
export default new Vuex.Store({
state: {
// other state fields
acceptedCookie: false
},
// rest of vuex configuration
})
// imports and configuration
export default new Vuex.Store({
// rest of vuex configuration
mutations: {
// other mutations
acceptCookie(state) {
state.acceptedCookie = true
}
}
})
export default {
methods: {
acceptCookie() {
this.$store.commit('acceptCookie')
}
},
computed: {
acceptedCookie() {
return this.$store.state.acceptedCookie
}
}
}
</script>
<template>
<div
v-if="!acceptedCookie"
class="flex flex-row bg-green-100 border text-center border-green-400
text-green-700 mt-8 px-4 md:px-8 py-3 rounded relative"
role="alert"
>
<!-- rest of template -->
<button
@click="acceptCookie()"
class="ml-auto align-center bg-transparent hover:bg-green-500
text-green-700 font-semibold font-sm hover:text-white py-2 px-4 border
border-green-500 hover:border-transparent rounded"
>
I agree
</button>
</div>
</template>
We have now got a cookie banner that shows until I agree is clicked, as shown in the following screenshot:
import {render, fireEvent} from '@testing-library/vue'
import CookieBanner from '../src/components/CookieBanner.vue'
import store from '../src/store'
test('Cookie Banner should display if store is initialised with it not dismissed', () => {
const {queryByText} = render(CookieBanner, { store })
expect(queryByText('Cookies Disclaimer')).toBeTruthy()
})
test('Cookie Banner should not display if store is initialised with it dismissed', () => {
const {queryByText} = render(CookieBanner, { store: {
state: {
acceptedCookie: true
}
} })
expect(queryByText('Cookies Disclaimer')).toBeFalsy()
})
test('Cookie Banner should hide on "I agree" button click', async () => {
const acceptCookie = jest.fn()
const {getByText} = render(CookieBanner, {
store: {
...store,
mutations: {
acceptCookie
}
}
})
await fireEvent.click(getByText('I agree'))
expect(acceptCookie).toHaveBeenCalledTimes(1)
})
The three tests we wrote pass when run with npm run test:unit __tests__/CookieBanner.test.js, as follows:
We've now seen how to test components that rely on Vuex for state and updates.
Next, we'll look at snapshot testing and see how it simplifies the testing of render output.
Snapshot tests provide a way to write tests for fast-changing pieces of code without keeping the assertion data inline with the test. They store snapshots instead.
Changes to a snapshot reflect changes to the output, which is quite useful for code reviews.
For example, we can add a snapshot test to the PostList.test.js file:
// imports and tests
test('Post List renders correctly', () => {
const wrapper = mount(PostList, {
propsData: {
posts: [
{
title: 'Title 1',
description: 'Description 1',
tags: ['react', 'vue']
},
{
title: 'Title 2',
description: 'Description 2',
tags: ['html', 'angularjs']
}
]
}
})
expect(wrapper.text()).toMatchSnapshot()
})
When we next run this test file, with npm run test:unit __tests__/PostList.test.js, we will get the following output:
The snapshot was written to __tests__/__snapshots__/PostList.test.js.snap, as follows:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Post List renders correctly 1`] = `
"Title 1 Description 1
#react
#vue
Title 2 Description 2
#html
#angularjs"
`;
This makes it easy to quickly see what the changes mean in terms of concrete output.
We've now seen how to use snapshot tests. Next, we'll put all the tools we learned in the chapter together to add a new page.
We have already built a post list page, a single-post view page, and a posts-by-tag page.
A great way to resurface old content on a blog is by implementing a good search functionality. We will add search to the PostList page:
We are now able to see the search form in the application, as follows:
Note
The solution for this activity can be found via this link.
Throughout this chapter, we've looked at different approaches to testing different types of Vue.js applications.
Testing in general is useful for empirically showing that the system is working. Unit tests are the cheapest to build and maintain and should be the base of testing functionality. System tests are the next level up in the testing pyramid and allow you to gain confidence that the majority of features are working as expected. End-to-end tests show that the main flows of the full system work.
We've seen how to unit test components, filters, component methods, and mixins, as well as testing through the layers, and testing component output in a black box fashion instead of inspecting component internals to test functionality. Using the Vue.js Testing library, we have tested advanced functionality, such as routing and applications, that leverage Vuex.
Finally, we looked at snapshot testing and saw how it can be an effective way to write tests for template-heavy chunks of code.
In the next chapter, we will look at end-to-end testing techniques that can be applied to Vue.js applications.
3.145.109.8