https://kentcdodds.com/blog/avoid-nesting-when-youre-testing
Photo by Kate Remmer
I want to show you something. What I'm going to show is a general testing principle, applied to a React component test. So even though the example is a React one, hopefully it helps communicate the concept properly.
Note: my point isn't that nesting is bad by itself, but rather that it naturally encourages using test hooks (such as beforeEach
) as a mechanism for code reuse which does lead to unmaintainable tests. Please read on...
Here's a React component that I want to test:
// login.js
import * as React from 'react'
function Login({ onSubmit }) {
const [error, setError] = React.useState('')
function handleSubmit(event) {
event.preventDefault()
const {
usernameInput: { value: username },
passwordInput: { value: password },
} = event.target.elements
if (!username) {
setError('username is required')
} else if (!password) {
setError('password is required')
} else {
setError('')
onSubmit({ username, password })
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameInput">Username</label>
<input id="usernameInput" />
</div>
<div>
<label htmlFor="passwordInput">Password</label>
<input id="passwordInput" type="password" />
</div>
<button type="submit">Submit</button>
</form>
{error ? <div role="alert">{error}</div> : null}
</div>
)
}
export default Login
And here's what that renders (it actually works, try it):
Username
Password
Here's a test suite that resembles the kind of testing I've seen over the years.
// __tests__/login.js
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import Login from '../login'
describe('Login', () => {
let utils,
handleSubmit,
user,
changeUsernameInput,
changePasswordInput,
clickSubmit
beforeEach(() => {
handleSubmit = jest.fn()
user = { username: 'michelle', password: 'smith' }
utils = render(<Login onSubmit={handleSubmit} />)
changeUsernameInput = (value) =>
userEvent.type(utils.getByLabelText(/username/i), value)
changePasswordInput = (value) =>
userEvent.type(utils.getByLabelText(/password/i), value)
clickSubmit = () => userEvent.click(utils.getByText(/submit/i))
})
describe('when username and password is provided', () => {
beforeEach(() => {
changeUsernameInput(user.username)
changePasswordInput(user.password)
})
describe('when the submit button is clicked', () => {
beforeEach(() => {
clickSubmit()
})
it('should call onSubmit with the username and password', () => {
expect(handleSubmit).toHaveBeenCalledTimes(1)
expect(handleSubmit).toHaveBeenCalledWith(user)
})
})
})
describe('when the password is not provided', () => {
beforeEach(() => {
changeUsernameInput(user.username)
})
describe('when the submit button is clicked', () => {
let errorMessage
beforeEach(() => {
clickSubmit()
errorMessage = utils.getByRole('alert')
})
it('should show an error message', () => {
expect(errorMessage).toHaveTextContent(/password is required/i)
})
})
})
describe('when the username is not provided', () => {
beforeEach(() => {
changePasswordInput(user.password)
})
describe('when the submit button is clicked', () => {
let errorMessage
beforeEach(() => {
clickSubmit()
errorMessage = utils.getByRole('alert')
})
it('should show an error message', () => {
expect(errorMessage).toHaveTextContent(/username is required/i)
})
})
})
})
That should give us 100% confidence that this component works and will continue to work as designed. And it does. But here are the things I don't like about that test:
I feel like the utilities like changeUsernameInput
and clickSubmit
can be nice, but the tests are simple enough that duplicating that code instead could simplify our test code a bit. It's just that the abstraction of the function doesn't really give us a whole lot of benefit for this small set of tests, and we incur the cost for maintainers to have to look around the file for where those functions are defined.
The tests above are written with Jest APIs, but you'll find similar APIs in all major JavaScript frameworks. I'm talking specifically about describe
which is used for grouping tests, beforeEach
for common setup/actions, and it
for the actual assertions.