Introduction
Test Driven Development is a software practice. TDD focuses on three (3) important things:
- Testing
- Coding
- Refactoring.
The goal of TDD is to ensure developers have a roadmap of their code outcome before writing the actual code. In TDD, you write a test (mostly unit tests). The test is expected to fail because there is no corresponding code. After writing your test, you are required to write your code or script. After, you can continuously refractor your codebase to successfully pass all your test cases. The test process is the driving force of software development. It helps build a resilient and optimized coding structure over a long time. TDD ensures developers write only the necessary codes required for a software or a codebase. TDD helps reduce breakage in applications while in production mode and improved productivity.
Unit Testing
TDD requires that you write unit tests often. Unit is mostly referred to as class or group of functions. Keeping your unit minimal is a good practice in TDD. Minimal units help in reducing debugging period. In a component-based app like Vue.js, the unit is a component.
To read more on Test Driven Development, get Test Driven Development: By Example by Kent Beck
Introduction to Node.js, Vue.js, VueTestUtils
Vue.js
Vue.js is a progressive framework for building user interfaces. Learning Vue.js requires an intermediate knowledge of HTML, CSS and Javascript. Grasping the basics before going into the framework might be the best decision in any chosen language of your choice. Vue.js is a javascript framework. For an introduction to Vue.js syntax, you can check out this Helloworld example by the Vue.js team. Vue.js is a component-based framework.
Node.js
Node.js is an open-source project that runs the V8 javascript engine, it is also a cross-platform runtime environment. Node.js has helped developers write server-side code. Node.js uses the javascript syntax. With a vast module of libraries, developers have shorter development time as most of the libraries handle bulky code contents. Node.js also have frameworks like Express, Fastify, FeatherJs, Socket.io, Strapi and others.
Vue Test Utils
How do we test our components? Earlier, we introduced units and for component-based apps, units are components. Vue.js is a component-based app needs components to be isolated to allow for testing.Vue test utils help with the isolation. Vue Test Utils is an official library of helper functions to help users test their Vue.js components. It provides some methods to mount and interact with Vue.js components in an isolated manner. We refer to this as a wrapper.
But what is a wrapper?
A wrapper is an abstraction of the mounted component. It provides some utility functions such as when users want to trigger a click or an event. We'll use this to execute some input ( props, store changes, etc.) so we can check that the output is correct (component rendering, Vue events, function calls, etc.).
Prerequisites
For this tutorial, you're required to have:
- Node.js installed.
- Also, we will be using Vue3 for this tutorial
- Vue test utils 2 (Vue test utils 1 target and earlier versions)
- A code editor.
Objectives
- Learn the basic principles of Test Driven Development
- Why you should test your Vue.js app
- Learn how to unit test a Vue.js app.
Setting up our environment
Vue3 gives us the opportunity to select unit tests while creating a vue project. You can follow the steps below for the manual installation.
For existing projects, you can use Vue CLI to set up Vue Test Utils in your current Vue app.
vue add unit-jest
npm install --save-dev @vue/test-utils
Your package.json
file should have added a new command.
[package.json]
{
"scripts": {
"test:unit": "vue-cli-service test:unit"
}
}
After installation of all relevant dependecies either manually or to existing projects, we proceed to our code editor.
Step 1 -- Setting up our files
After opening our code in our code editor, we will go to the test directory. Test directory is a root folder in our <project-name>
. Open the unit folder, then you can create a new file (<project-name>/tests/unit/<file-name.spec.js>
). It is a good practice to name the file as the component. Initially, there is an example.spec.js
file in the unit folder. Remember the goal of TDD is testing before code. You will create a boilerplate for the vue component in the component folder (<project-name>/src/component/loginpage.vue
). The boilerplate structureis provided below.
[<project-name>/src/component/loginpage.vue]
<template>
<div></div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
In our spec file, we are importing our vue component and making use of the Vue test utils.
import{ shallowMount } from '@vue/test-utils'
import Login from '@/components/Login'
Step 2 -- First test
Our first test is to ensure if our Login components displays a form.
[<project-name>/tests/unit/loginpage.spec.js]
import { shallowMount } from '@vue/test-utils'
import Login from '@/components/Login'
describe('login.vue', () => {
test('should show the form element on the user output', () => {
const wrapper = shallowMount(Login)
expect(wrapper.find("form").exists()).toBe(true)
}),
})
Running our test using the yarn test:unit --watch
or npm run test:unit
command, our test failed!
FAIL tests/unit/loginpage.spec.js
login.vue
✕ should show the form element on the screen (13ms)
● login.vue › should show the form element on the screen
Cannot call isVisible on an empty DOMWrapper.
Expected: true
Received: false
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.174s
Ran all test suites related to changed files.
Notice the error? Cannot call isVisible
on an empty DOMWrapper. We wrote a test without the code it will act on. Our component booilerplate is empty. To resolve this, we simply go to boilerplate our and write this code.
[<project-name>/src/component/loginpage.vue]
<template>
<div>
<form action="">
</form>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
Our test should pass now. Congratulations! You just wrote your first successful test!
PASS tests/unit/loginpage.spec.js
login.vue
✓ should show the form element on the screen (60ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.273s, estimated 9s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
Step 3 -- Further testing
Let's go further by checking if our input field exists.
[<project-name>/tests/unit/loginpage.spec.js]
test('should contain input fields', () => {
const wrapper = shallowMount(Login)
expect(wrapper.find('form > input').exists()).toBe(true)
})
test('form should contain input fields with type="text"', () => {
const wrapper = shallowMount(Login)
expect(wrapper.get('input[type=tjavascriptext]').exists()).toBe(true)
})
Our test failed as there was no input field present in our form element.
FAIL tests/unit/loginpage.spec.js
login.vue
✓ should show the form element on the screen (10ms)
✕ should contain input fields (5ms)
✕ form should contain input fields with type="text" (10ms)
● login.vue › should contain input fields
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: false
● login.vue › form should contain input fields with type="text"
Unable to get input[type=text] within: <div>
Test Suites: 1 failed, 1 total
Tests: 2 failed, 1 passed, 3 total
Snapshots: 0 total
Time: 3.549s
Ran all test suites related to changed files.
Now let's open our Login component
and add some codes.
[<project-name>/src/component/loginpage.vue]
<template>
<div>
<form action="">
<input type="text" name="" id="username" placeholder="Username">
</form>
</div>
</template>
Our test passed!
PASS tests/unit/loginpage.spec.js
login.vue
✓ should show the form element on the screen (13ms)
✓ should contain input fields (2ms)
✓ form should contain input fields with type="text" (2ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.805s, estimated 2s
Ran all test suites related to changed files.
A bonus test is confirming the attribute of our input field. The get()
function allows for parameters. We can check for tag attributes like type=text
. isVisible check the visibility status (showing on the user output device). Though isVisible()
is deprecated, latest release of Vue still accept it.
Our last test! Testing if our button triggers a click event. We trigger the click event listener, so that the Component executes the submit method. We use await to make sure the action is being reflected by Vue.
[<project-name>/tests/unit/loginpage.spec.js]
test('button trigger event', async () => {
await wrapper.find('form > button').trigger('click')
expect(wrapper.emitted()).toHaveProperty('submit')
})
We have a failed test again.
FAIL tests/unit/loginpage.spec.js
login.vue
✓ should show the form element on the screen (12ms)
✓ should contain input fields (3ms)
✓ form should contain input fields with type="text" (1ms)
✕ button trigger event (4ms)
● login.vue › button trigger event
Cannot call trigger on an empty DOMWrapper.
Test Suites: 1 failed, 1 total
Tests: 1 failed, 3 passed, 4 total
Snapshots: 0 total
Time: 3s
Ran all test suites related to changed files.
Our triggering test failed as we don't have a corresponding button element in our login component. In our login component, we are going to add the button element.
[<project-name>/src/component/loginpage.vue]
<template>
<div>
<form action="">
<input type="text" name="" id="username" placeholder="Username">
<button @click="submit">Submit</button>
</form>
</div>
</template>
Our test failed as we do not have a corresponding method in our component boilerplate.
[<project-name>/src/component/loginpage.vue]
<template>
<div>
<form action="">
<input type="text" name="" id="username" placeholder="Username">
<button @click="submit">Submit</button>
</form>
</div>
</template>
<script>
export default {
methods: {
submit() {
this.$emit('submit', this.email)
}
}
}
</script>
Our complete login component. Notice the additional change to the script section of the our component. Now all our tests should pass.
PASS tests/unit/loginpage.spec.js
login.vue
✓ should show the form element on the screen (11ms)
✓ should contain input fields (2ms)
✓ form should contain input fields with type="text" (1ms)
✓ button trigger event (5ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.88s, estimated 2s
Ran all test suites.
To make our test codes easier, We can refractor by making the wrapper variable a global variable and our tests still passed.
Due to the isVisible being deprecated, we can use the exists()
function. Testing depends on your contract with your end user.
You need to be sure "DO I CARE IF THIS CHANGE?" If you care, test, else move to the next detail. TDD helps write robust tests (not too many, not too few).
Conclusion
- An introduction to TEST DRIVEN DEVELOPMENT
- Benefit of TDD.
- Setting up our Vue project.
- Writing our first tests suites successfully.
To get the full Vue project, clone it on GitHub
Top comments (2)
I usually work in a small team, like 2 people most in the fe. Usually the deadline tight, like less then a month or so. I really want to try implement this, but is it possible? Any suggestion?
Yes, the goal is to write the tests before the actual code. It's not compulsory you write test for the source code, but anything worth testing