I’ve been quite a big fan of using TDD (Test-Driven Development) in my work, and I thought that it could be nice to write a blog on how I use it.
What is TDD?
TDD has been around for a while, and it’s a software development technique that is based on the idea of writing tests before you write the actual code. Originally TDD was introduced by Kent Beck. Martin Fowler has written quite a detailed explanation on it in this blog post.
One of the important aspects of TDD is that you actually write failing tests first before you write the actual code. This is a bit counter-intuitive, but it’s actually a really good way to make sure that you’re not writing any false positives.
Creating a new Vue.js project
For this post, I’ll be starting completely from the beginning,
and install a new Vue.js project. I’ll be using npm
for this, but you can
use yarn
as well. To create a new Vue.js project, you can run the following:
npm create vue@latest
You’re free to use whatever setup you like, but I prefer using TypeScript and ESLint. I’ll be using the following setup:
✔ Project name: … tdd-project
✔ Add TypeScript? … Yes
✔ Add JSX Support? … Yes
✔ Add Vue Router for Single Page Application development? Yes
✔ Add Pinia for state management? Yes
✔ Add Vitest for Unit Testing? Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? Yes
✔ Add Prettier for code formatting? No
Writing the first test
The first small change we’ll make is modify package.json
and the script for watching tests. To do this,
add the following to package.json
:
"test:unit:watch": "vitest watch",
And then run npm run test:unit:watch
. This will start watching
your project and tests, and your terminal should look like this:
As in every blog post on frontend frameworks, we’ll start by creating a small counter application. The steps we’ll take for this application is:
- Create test file for
Counter.spec.js
- Create
Counter.vue
component - Create the functionality adding and subtracting from the counter
Now let’s start by creating a new file Counter.spec.js
. Although the project
uses TS, I won’t be using it in the tests.
Your project structure should look something like this:
src/
├── App.vue
├── components
│ └── Counter.vue
│ └── __tests__
│ └── Counter.spec.js
Now let’s start by writing the first test. First we want to make sure that the counter value is actually shown in the component. To do this, we’ll write the following integration test:
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'
describe('Counter', () => {
describe('Integration', () => {
it('renders count value properly', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Current counter value: 0')
})
})
})
Since the Vue component Counter.vue
looks like this:
<template>
<div>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
const counter = ref(0)
</script>
The test will fail, since we’re not rendering the counter value anywhere.
If we modify the component template to look like this:
<template>
<div>
Current counter value: {{ counter }}
</div>
</template>
The test will pass!
We’ve got our first test passing, and we can now move on to the next step,
which is implementing the functionality for adding and subtracting from the counter. For this,
we’ll create a small unit test using shallowMount
:
describe('Unit', () => {
it('should increment count value properly', () => {
const wrapper = shallowMount(Counter)
wrapper.vm.increment()
expect(wrapper.vm.counter).toBe(1)
wrapper.vm.increment()
expect(wrapper.vm.counter).toBe(2)
})
it('should decrement count value properly', () => {
const wrapper = shallowMount(Counter)
wrapper.vm.decrement()
expect(wrapper.vm.counter).toBe(-1)
wrapper.vm.decrement()
expect(wrapper.vm.counter).toBe(-2)
})
})
and at the same time, let’s create two new functions in the component:
const increment = () => {
console.log(counter)
}
const decrement = () => {
console.log(counter)
}
Now we’ve got two new failing tests since it’s not being incremented:
Let’s fix this by modifying the functions to look like this:
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
And we’ve got passing tests!
Now we’ve tested that the counter is being incremented and decremented properly, and that the value is actually shown.
One step of TDD is actually refactoring the code, and we can do this by modifying the increment function:
const increment = () => {
counter.value = counter.value + 1
}
And we still have passing tests.
Mocking Vue3 computed properties
Let’s say that the increment function would depend on a computed property instead, which could for example just return the counter value. It could look something like this:
const someProps = computed(() => {
return counter.value
})
Now we still want to ensure that the increment function works properly, but we don’t want the test to depend on the computed property. Unfortunately this is not possible in Vue3 anymore according to this StackOverflow.
Let’s say that if we increment, but we have a computed property that returns the counter value + 2.
const someComputed = computed(() => {
return counter.value + 2
})
And let’s say we want the increment function to increment if someComputed
is
less than 10, but if it’s more than 10, we want to set the counter value to 0.
We don’t wanna do some stupid incrementing until the value is more than 10,
rather let’s just mock the counter value to return 10.
it('should increment count value properly', () => {
const wrapper = shallowMount(Counter)
wrapper.vm.counter = 11
wrapper.vm.increment()
expect(wrapper.vm.counter).toBe(0)
})
Now we have a test that we can check that if the counter value is more than 10, it will be set to 0.
And the function could look something like this:
const increment = () => {
if (someComputed.value > 10) {
counter.value = 0
return
}
counter.value = counter.value + 1
}
So what this means is that we cannot any longer test computed properties the same way as in Vue2, but we need to mock the underlying property instead.
Wrap-up
And this is how you can use TDD in Vue.js. I hope you enjoyed this blog post.