Front-end Testing and a tale of three users

How should we think about UI components when testing them?

If I gave you a really complex function such as

function sum (a, b) {
return a + b
}
test('sums two numbers', () => {
expect(sum(1, 2)).toBe(3)
})

test('JavaScript being JavaScript', () => {
expect(sum(null, undefined)).toBe(NaN)
expect(sum(null, null)).toBe(0)
})

Enter our code users

We can’t answer those questions (yet), but we can explore the problem from another perspective. First of all, let’s define who’s gonna use our component:

  1. The developer. The person who’s gonna import your components and use them to developer their app. (Note: you are your own code’s consumer. Let it sink.)

Back to inputs and outputs

Now that we defined our users, what inputs and outputs could we expect from them? (Notice how we keep building knowledge from previous truths):

Inputs
Interactions: clicking, typing… any “human” interaction
Props: The arguments a component receives
Outputs
Examples: Side Effects* HTTP requests, Cookies, console.log() DOM elements: <input>, <div>, whatever. Elements on the screen

Be careful of the dreaded third user

tl;dr: If a test relies on implementation details, then the test becomes a third user. And you’re gonna need to please it.

<template>
<div>
<p class="paragraph">Times clicked: {{ count }}</p>
<button @click="increment">increment</button>
</div>
</template>

<script>
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
}
}
</script>

Don’t do this

import { mount } from '@vue/test-utils'

test('text updates on clicking', () => {
const wrapper = mount(Counter)
const paragraph = wrapper.find('.counter')

expect(paragraph.text()).toBe('Times clicked: 0')

wrapper.vm.increment()
wrapper.vm.increment()

expect(paragraph.text()).toBe('Times clicked: 2')
})

Instead, do this

import { render, fireEvent } from '@testing-library/vue'

test('text updates on clicking', async () => {
const { getByText } = render(Counter)

// getByText returns the first matching DOM node for the
// provided text, and throws an error if there's no match
// or if more than one element is found.
getByText('Times clicked: 0')

const button = getByText('increment')

// Dispatch a native click event to our button element.
await fireEvent.click(button)
await fireEvent.click(button)

getByText('Times clicked: 2')
})

Recap

  1. UI components have two consumers: your end users, and the developers using them.
  2. Think of components as you think of functions: black boxes that receive inputs and yield outputs.
  3. There’s a hidden third user for your component: a test. If your test is doing something different from end users and developers, then you need to take that into account.
  4. Your testing tools should help you stay on track. Vue Testing Library (and all the Testing Library toolset) does a pretty good job achieving that goal.

Words matter – Software product development, Front-end, UX, design, lean, agile and everything in between. https://afontcu.dev