DEV Community

Cover image for Test Driven Development: How to test using Vue.js, VueTestUtils and Jest
Sagacité
Sagacité

Posted on

Test Driven Development: How to test using Vue.js, VueTestUtils and Jest

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:

  1. Node.js installed.
  2. Also, we will be using Vue3 for this tutorial
  3. Vue test utils 2 (Vue test utils 1 target and earlier versions)
  4. 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
Enter fullscreen mode Exit fullscreen mode

Your package.json file should have added a new command.

[package.json]
{
  "scripts": {
    "test:unit": "vue-cli-service test:unit"
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)
    }),
})
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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')
})
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

  1. An introduction to TEST DRIVEN DEVELOPMENT
  2. Benefit of TDD.
  3. Setting up our Vue project.
  4. Writing our first tests suites successfully.

To get the full Vue project, clone it on GitHub

Discussion (2)

Collapse
mirrorbottle profile image
MirrorBottle

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?

Collapse
fikkyman1 profile image
Sagacité Author

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