This tutorial covers:
- Creating and setting up a Vue.js application
- Writing automated tests for Vue components
- Setting up a continuous integration pipeline
One of the leading frameworks in the JavaScript community, Vue.js is a progressive framework for building reusable components for the web user interface. Its intuitive API and robust flexibility for handling front-end logic are just two of the reasons that Vue has been adopted by developers worldwide.
In this tutorial, I will lead you through building a simple listing application that shows the names and roles of users. I will show you how to write tests for the application. Finally, you will configure a continuous integration pipeline for automating testing.
Prerequisites
For this tutorial, you will need:
- Node.js installed on your system, preferably version >=14.18.0.
- A CircleCI account.
- A GitHub account.
- Vue CLI installed. This tutorial uses Vue CLI 5.
- Familiarity with building apps with Vue.js.
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
Getting started
You will create a new Vue.js project by using Vue CLI. From the terminal run this command:
vue create vue-user-app
You will be prompted to answer a few questions. For this tutorial, use the responses shown here:
- Please pick a preset: Manually select features
- Check the features needed for your project: Babel, Linter, Unit
- Choose a version of Vue.js that you want to start the project with 3.x
- Pick a linter / formatter config: ESLint with error prevention only
- Pick additional lint features: Lint on save
- Pick a unit testing solution: Jest
- Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
- Save this as a preset for future projects? No
The Vue CLI will install the Vue application and its dependencies as you specified.
Go to the new project and run it using these commands:
cd vue-user-app
npm run serve
You can review the application in your browser at http://localhost:8080
.
This renders the default homepage for a new Vue.js application. You will change this by creating new reusable components in the next section of this tutorial.
Stop the application from running using CTRL + C.
Creating reusable components
Vue.js components contain three different sections for building web applications. They are:
<template></template>
<script></script>
<style></style>
These sections help create a proper structure for the view, business logic, and styling.
Creating user component
The first component that you will add to the application is for creating and listing users. This component houses a form with an input field to accept the name of a particular user. When the form is submitted, the details from the input field are pushed to a dummy users
array created for testing purposes.
To begin, create a new file named UserList.vue
within the ./src/components
folder. Open this new file and paste this content:
<template>
<div class="container">
<div class="page-title">
<h3>{{ message }}</h3>
</div>
<div class="row">
<div
class="col-md-4"
v-for="user in users"
:key="user.id"
data-user="user"
>
<div class="m-portlet m-portlet--full-height">
<div class="m-portlet__body">
<div class="tab-content">
<div class="tab-pane active" id="m_widget4_tab1_content">
<div class="m-widget4 m-widget4--progress">
<div class="m-widget4__item">
<div class="m-widget4__img m-widget4__img--pic">
<img
src="https://bootdey.com/img/Content/avatar/avatar1.png"
alt=""
/>
</div>
<div class="m-widget4__info">
<span class="m-widget4__title"> {{ user.name }} </span>
<br />
<span class="m-widget4__sub">
{{ user.title }}
</span>
</div>
<div class="m-widget4__ext">
<button
@click="deleteUser(user)"
class="btn btn-primary"
data-cy="taskDelete"
id="deleteForm"
>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<form id="form" @submit.prevent="createUser">
<input id="new-user" v-model="newUser" class="form-control" />
</form>
</div>
</div>
</template>
This is the <template></template>
section that renders the users list to the view. It also contains an input field to post the name of a new user.
Next, paste this code just after the end of the </template>
tag:
<script>
export default {
props: ["message"],
data() {
return {
newUser: "",
users: [
{
id: 1,
name: "Anna Strong",
title: "Software Engineer",
},
{
id: 2,
name: "John Doe",
title: "Technical Writer",
},
],
};
},
methods: {
createUser() {
this.users.push({
id: 3,
name: this.newUser,
title: "Crypto Expert",
});
this.newUser = "";
},
deleteUser(user) {
const newList = this.users.filter((u) => user.id !== u.id);
this.users = newList;
},
},
};
</script>
This defines a users
array with dummy data to be rendered on the page. The createUser()
method receives the details of a new user via the input field and pushes it to the users
array. You also defined a method named deleteUser()
which accepts a user
object as a parameter and removes it from the user list when invoked.
Creating the header component
To create a component for the Header section of the view, go to ./src/components
folder and create a new file named NavBar.vue
. Paste this code into it:
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div
class="collapse navbar-collapse justify-content-md-center"
id="navbarsExample08"
>
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="#">
Users Listing App <span class="sr-only">(current)</span></a
>
</li>
</ul>
</div>
</nav>
</template>
Updating the app component
Open the application’s AppComponent
and update it by including the links to both the NavBar
and UserList
components. Replace its content with this:
<template>
<div>
<NavBar />
<UserList />
<div class="container"></div>
</div>
</template>
<script>
import NavBar from "./components/NavBar.vue";
import UserList from "./components/UserList.vue";
export default {
name: "App",
components: { NavBar, UserList },
};
</script>
<style>
body {
background: #eee;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.page-title {
margin: 15px 15px;
}
.m-portlet {
margin-bottom: 2.2rem;
}
.m-portlet {
-webkit-box-shadow: 0 1px 15px 1px rgba(113, 106, 202, 0.08);
-moz-box-shadow: 0 1px 15px 1px rgba(113, 106, 202, 0.08);
box-shadow: 0 1px 15px 1px rgba(113, 106, 202, 0.08);
background-color: #fff;
}
.m-portlet .m-portlet__head {
border-bottom: 1px solid #ebedf2;
}
.m-widget4 .m-widget4__item {
display: table;
padding-top: 1.15rem;
padding-bottom: 1.25rem;
}
.m-widget4 .m-widget4__item .m-widget4__img {
display: table-cell;
vertical-align: middle;
}
.m-widget4 .m-widget4__item .m-widget4__img.m-widget4__img--logo img {
width: 3.5rem;
border-radius: 50%;
}
.m-widget4 .m-widget4__item .m-widget4__img.m-widget4__img--pic img {
width: 4rem;
border-radius: 50%;
}
.m-widget4 .m-widget4__item .m-widget4__img.m-widget4__img--icon img {
width: 2.1rem;
}
.m-widget4 .m-widget4__item .m-widget4__info {
display: table-cell;
width: 100%;
padding-left: 1.2rem;
padding-right: 1.2rem;
font-size: 1rem;
vertical-align: middle;
}
.m-widget4 .m-widget4__item .m-widget4__info .m-widget4__title {
font-size: 1rem;
font-weight: bold;
}
.m-widget4.m-widget4--progress .m-widget4__info {
width: 50%;
}
</style>
This includes a <style></style>
section to include styling for the app.
Include Bootstrap
Open the index.html
file within the public folder and include the CDN file for Bootstrap. This is just to give the page some default style.
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css"
integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn"
crossorigin="anonymous"
/>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Now, run the application again with npm run serve
. Visit https://localhost:8080
.
Now that your application is up and running you can start unit testing for the UserList
component.
Unit testing of Vue.js component
There are many, many testing frameworks for JavaScript applications. Jest stands out as one of the most popular. For Vue.js, Vue Test Utils (VTU) is the preferred testing library. That makes sense, because the VTU testing library is built on Jest. It was designed to simplify testing Vue.js components by providing utility functions.
When you created this project and selected Jest as the unit testing solution, Vue CLI installed vue-test-utils
, jest
, and other testing libraries. The following directory was also created:
test/unit
: This directory will house all the unit tests. Jest will search for your unit tests files here once the command for testing is issued.
Writing tests for the application
In this section, we will write the unit test for the UserList
component. In that component we want to:
- Mount the component and check that it can render props passed to it.
- Find elements within the component and render the user list.
- Submit a form, then create a new user and add it to the list of existing users.
To begin, go to the test/unit
folder and rename the example.spec.js
file to user.spec.js
. Open the file and replace its content with this:
import { mount } from "@vue/test-utils";
import UserList from "@/components/UserList.vue";
describe("User List component unit tests: ", () => {
it("renders props when passed", () => {
const message = "new message";
const wrapper = mount(UserList, {
props: { message },
});
expect(wrapper.text()).toMatch(message);
});
test("Renders the list", () => {
const wrapper = mount(UserList);
const name = "Anna Strong";
const user = wrapper.get('[data-user="user"]');
expect(user.text()).toContain(name);
expect(wrapper.findAll('[data-user="user"]')).toHaveLength(2);
});
test("creates a user", async () => {
const wrapper = mount(UserList);
const newName = "John Doe";
await wrapper.get('[id="new-user"]').setValue(newName);
await wrapper.get('[id="form"]').trigger("submit");
expect(wrapper.findAll('[data-user="user"]')).toHaveLength(3);
});
});
Within this file, we imported a function named mount
from vue-test-utils
library to help us mount an instance of the UserList
component.
We started by writing a test to assert that the component can render a prop passed into it from a parent component. Next, we targeted the data attribute within the view of the UserList
component, ensured that it contains a specific name of one of the users, and that it renders the default length from the users
array.
Lastly, we created a test function to ensure that a new user can be created within the component.
Running the test locally
To confirm that the defined tests are passing, enter this command from the terminal:
npm run test:unit
This is the terminal output:
> vue-user-app@0.1.0 test:unit
> vue-cli-service test:unit
PASS tests/unit/user.spec.js
User List component unit tests:
✓ renders props when passed (33 ms)
✓ Renders the list (19 ms)
✓ creates a user (27 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.924 s, estimated 1 s
Ran all test suites.
In the next section, you will automate this test using CircleCI.
Automating the test
In this section you will create a configuration file for your project’s continuous integration pipeline.
Create a .circleci
folder at the root of the project and create a new file named config.yml
within it. Add this content:
version: 2.1
jobs:
build-and-test:
working_directory: ~/project
docker:
- image: cimg/node:17.4.0
steps:
- checkout
- run:
name: Update NPM
command: "sudo npm install -g npm"
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Install Dependencies
command: npm install --legacy-peer-deps
- save_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: Run test for the application
command: npm run test:unit
workflows:
build-and-test:
jobs:
- build-and-test
This file defines a job to build and run the test command for your project. This job:
- Pulls in the
cimg/node:17.4.0
Docker image from the CircleCI Docker image registry - Uses the image to install all the project’s dependencies
- Runs the test
Now that the configuration file has been properly set up, you need to set up a repository on GitHub and link the project to CircleCI. Review Pushing your project to GitHub for step-by-step instructions.
Setting up the project on CircleCI
Log in to your CircleCI account with the linked GitHub account to view all your repositories on the projects’ dashboard. Search for the vue-user-app
project and click Set Up Project to continue.
You will be prompted to choose a configuration file. Select the .circleci/config.yml
file in your repo and enter the name of the branch where your code is stored. Click Set Up Project to start the workflow.
This will initialize the workflow and run the test for your project.
Conclusion
In this tutorial, we built a listing application with Vue.js and covered the required steps to write a unit test for its component. We then used CircleCI infrastructure to configure a continuous integration pipeline to automate the test.
I hope that you found this tutorial helpful. Check here on GitHub for the complete source code for the project built in this guide.
Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.