I've been focused so much on testing within VueJS lately that I really wanted to get some thoughts about it down in a post.
This especially occurred to me yesterday as I was writing a new component, and approached it purely from a Test Driven Design (TDD) direction.
When I first heard of TDD it seemed totally unappealing. I couldn't mentally integrate writing tests before code.
Before getting started, let me clarify that this is not a tutorial on tests or testing in VueJS. I'm assuming that you may have taken a stab at testing and became frustrated or didn't see the use in it (a position I was in for quite a long time myself).
Even though this post isn't about how you go about writing tests, Vue has tremendous documentation on it.
Here's my personal favorite resources on writing tests in VueJS:
Vue Test Utils
Vue Testing Handbook by lmiller1990
Edd Yerburgh's Testing Vue.JS Applications
To start with, my non-test driven way of coding would usually entail writing lots of lines of code, and when I thought of another feautre I wanted then I'd write the appropriately needed functionality.
For example, if I was writing a page for a calendar, I would start first with the main Vue file:
<template>
<div>
</div>
</template>
Then wholesale write some code to accomplish what I wanted:
<template>
<div>
<div>{{ title }}</div>
<div>{{ currentDate }}</div>
<div>{{ dayOfWeek }}</div>
<div>
</template>
<script>
export default {
name: 'Test',
data: () => ({
title: 'The title'
}),
computed: {
currentDate () {
return new Date()
},
dayOfWeek () {
return this.currentDate.getDay()
}
}
}
</script>
Alright, great, a nice and easy to understand a Vue component file... but actually, I just realized I want this calendar to show multiple days of the week.
And what about date formats?
And what about in various languages?
And...
And...
And...
Anyone who's built an application should know this feeling. And while this example is very straight forward, what happens when it's not?
Something that is incredibly appealing about Test Driven Design is that it slows down the process.
Sitting down and slamming your keyboard over and over again, may get digital letters, numbers, and symbols to pop up on your monitor, but is it necessarily effective in the long run?
Of course, if you have a pretty small, simple project, then by all means feel free to skip TDD, but keep in mind the point of TDD is to break your code down into small manageable parts.
Which leads to the first major mental hurdle about testing I'd like to make:
Break Your Code Down Into Easy To Manage Pieces
It's so hard not to indulge the awesomeness of programming and write super complex functions to handle whatever your component may need.
Without a doubt coding is fun, interesting, and compelling all at the same time. Its power will course through electronic veins and arteries like lightning combusting a tree in the middle of a lonely forest.
But it wasn't until I came across this slightly unrelated example in VueJS's official style guide for simple computed properties that gave me a programmatic epiphany:
Bad
computed: {
price: function () {
var basePrice = this.manufactureCost / (1 - this.profitMargin)
return (
basePrice -
basePrice * (this.discountPercent || 0)
)
}
}
Good
computed: {
basePrice: function () {
return this.manufactureCost / (1 - this.profitMargin)
},
discount: function () {
return this.basePrice * (this.discountPercent || 0)
},
finalPrice: function () {
return this.basePrice - this.discount
}
}
I've seen that bad example in my code a million times, and without getting too lost in the details, the point is to focus on the concept of breaking things down to make them easier to digest.
While this is a good rule for almost every aspect of writing code, it also makes a lot of sense in testing, and I would argue is a good place to start to write "testable code."
For example, imagine a method that you want to test that looks like this:
testCode (value) {
const variable = value * 2
const variable2 = this.computedValue * 3
const variable3 = this.transformValue(value)
const variable 4 = async function() {
await return axios(...)
...
You get the point.
I used to think that there was a flaw with testing while attempting to tackle a complex function that I'd written.
As I attempted to write my way out of a maze of nested functions and asynchronous behaviors that functioned the way I wanted, I'd find that it wasn't easy to write tests at all for it.
Looking at the above testCode
method, there's at least four different aspects that need to be mocked to get a single test to work.
Unfortunately, you'll find yourself wondering why even write the test at all, since it feels as if the entire function requires every aspect to be mocked anyway.
Writing Testable Code, A Subset of Test Driven Design
The more that I work with tests the more I'm willing to believe that tests don't hold much value unless your code was written with Test Driven Design.
On the one hand, it is entirely possible to revisit your code, and try to think your way through what tests need to be written by analyzing what's there.
Take the following example:
<template>
<div :class="{ myClass: isTrue }">
{{ companyName }}
<div :style="{ hidden: showDiv }">
{{ warning }}
</div>
<button @click="isTrue = !isTrue">
Click me!
</button>
</div>
</template>
<style>
export default {
props: ['companyName'],
name: 'Test',
data: () => ({
isTrue: true
})
computed: {
showDiv () {
return this.isTrue
},
warning () {
return `${this.companyName} is a warning.`
}
}
}
</style>
Coming upon a very short snippet of code like this definitely makes it easy to breakdown code to write tests.
You may even just build your tests by analyzing the component from top to bottom.
- When
isTrue
is true does it rendermyClass
in the maindiv
's classes. - Does the
companyName
prop render correctly? - Does the rendered style show properly from the
showDiv
computed property? - Does the
warning
computed property return thecompanyName
prop properly in the string that's defined?
Etc.
The problem with building a component's tests after the code has been written is that it doesn't tell the story of the component.
While the tests above may address the technical specifications of the code, it doesn't address the intentions of the component.
Instead, let's look at how Test Driven Design writes the component, and pay attention to how it helps drive the mental clarity of what is being written, not just how it's being written.
1. Make sure the component renders correctly, and this is known as a sanity test and should be the first test in every test file.
Test:
import TestComponent from 'src/components/TestComponent.vue'
const wrapper = shallowMount(TestComponent)
it('sanity test', () => {
expect(wrapper.isVueInstance())
.toBe(true)
})
Corresponding code: Creating a file in the components folder named TestComponent.vue.
2. Insure you're receiving the correct props.
This gets tested before rendering because logically, you would need to receive the prop before it can be used (meaning you wouldn't be able to test anything dependent on it).
Test:
it('receives companyName prop', () => {
wrapper.setProps({ companyName: 'test' })
expect(wrapper.props().companyName)
.toEqual('test')
})
Corresponding Code:
<template>
<div>
</div>
</template>
<script>
props: ['companyName']
</script>
3. Set proper checks for companyName prop, not allowing for non-alphanumeric characters to be used in name.
Test:
it('companyName prop checks for validity', () => {
const value = wrapper.vm.$options.props.companyName
expect(value.required)
.toBeTruthy()
expect(value.type)
.toBe(String)
expect(value.validator && value.validator('test name'))
.toBeTruthy()
expect(value.validator && value.validator('test ^'))
.toBeFalsy()
})
Corresponding Code:
<script>
props: {
type: String,
required: true,
validator: v => v.match(/[^a-zA-Z0-9 ]/g) > 0
}
</script>
Note: Notice how each test only creates the very smallest amount of code needed to satisfy it. While there may be a concern of tedium, the long term clarity of development will be evident.
4. The companyName prop renders
Test:
it('companyName prop renders validly', () => {
wrapper.setProps({ companyName: 'test name' })
expect(wrapper.find('div').text())
.toContain('test name')
})
Corresponding Code:
<template>
<div>
{{ companyName }}
</div>
</template>
As you can see over just four tests, there's a much clearer direction to what is being built by starting with tests first.
The next tests would progress in an order similar to this:
- warning computed property is returning the correct string
- when clicking the button does it change the data value
- does the hidden class render when the button is clicked
- etc.
Each test dictating what the following test would be, like a digital karmic flow.
Building the component from bottom up allows for a much more succinct connection between all the aspects of the component. Your code will follow a logical progression of steps, rather than a "build as needed" style of development.
The simplicity will allow for longer-term understanding of what's being built, plus all the positive benefits of having 100% coverage on your code (since each line only exists because there is a test for it).
Top comments (2)
Hi Ricardo. Thanks for posting this, testing is one of the trickiest areas of Vue.
That said, I don't really see the point in testing this way. Most of the tests here are either testing Vue core functionality, which already has tests, or are duplicating other error mechanisms. For example, if you mess up your props or text interpolations, you'll get a compilation error that tells you the same info as what these tests do.
I do believe in unit tests, I just think they're only valueable when they're more abstract i.e. testing the inputs and outputs of the component, not implementation details.
For this component, I think one snapshot test is all that's needed!
Hi Anthony, fan of your work and I appreciate the feedback.
Yes, I absolutely agree, and I really hope the intention of the post isn't lost in the details of the examples I used.
I'm more trying to convey the concept of what it looks like to have tests drive what you code rather then going back and writing tests after writing code.
I hope the examples didn't distract from the concept, even though I think testing a props validator is useful.
But I'll go back and choose some better examples if you think the point gets that lost.