Testing UI Components
Why write tests?
Why write tests?
- To "cover code" and boost our coverage metrics
Why write tests?
To "cover code" and boost our coverage metrics
Why write tests?
To "cover code" and boost our coverage metrics- It's a "best practice"
Why write tests?
To "cover code" and boost our coverage metricsIt's a "best practice"
Why write tests?
To increase confidence in our code and prevent it from breaking
Enzyme
Enzyme
Enzyme
<Counter />
Enzyme
<Counter />
Enzyme
Enzyme
<Counter />
Enzyme
Enzyme
<Counter />
Enzyme
Enzyme
<Counter />
Enzyme
😔
Enzyme
<Counter />
Enzyme
😔
Enzyme
<Counter />
Enzyme
😔
Enzyme
<Counter />
Enzyme
😔
jsdom
jsdom
<Counter />
Enzyme
😔
jsdom
<Counter />
jsdom
<Counter />
jsdom
<Counter />
jsdom
<Counter />
jsdom
test("Adding a to-do", () => {
ReactDOM.render(<TodoApp />, document.body)
const addButton = document.querySelector('button')
const input = document.querySelector('input')
expect(addButton.disabled).toBe(true)
expect(input.value).toBe('')
input.value = 'Learn how to test'
addButton.click()
const newTodo = document.querySelector('ul.todo-list > li')
expect(newTodo).toBeDefined()
expect(newTodo.innerHTML).toBe('Learn how to test')
})
jsdom
@testing-library/react
@testing-library/jest-dom
jsdom
@testing-library/react
@testing-library/jest-dom
import { render, screen, userEvent } from "@testing-library/react";
test("Adding a to-do", () => {
render(<TodoApp />);
const addButton = screen.getByText("Add To-Do", { selector: "button" });
const input = screen.getByLabelText("To-Do text:");
expect(addButton).toBeDisabled();
expect(input).toHaveValue("");
userEvent.type(input, "Learn how to test");
userEvent.click(addButton);
expect(
screen.getByText("Learn how to test", { selector: "li" })
).toBeInTheDocument();
});
jsdom
@testing-library/react
@testing-library/jest-dom
import { render, screen, userEvent } from "@testing-library/react";
test("Adding a to-do", () => {
render(<TodoApp />);
const addButton = screen.getByText("Add To-Do", { selector: "button" });
const input = screen.getByLabelText("To-Do text:");
expect(addButton).toBeDisabled();
expect(input).toHaveValue("");
userEvent.type(input, "Learn how to test");
userEvent.click(addButton);
expect(
screen.getByText("Learn how to test", { selector: "li" })
).toBeInTheDocument();
});
jsdom
test("Adding a to-do", () => {
ReactDOM.render(<TodoApp />, document.body);
const addButton =
document.querySelector('button');
const input =
document.querySelector('input');
expect(addButton.disabled).toBe(true);
expect(input.value).toBe('');
input.value = 'Learn how to test';
addButton.click();
const newTodo = document.querySelector(
'ul.todo-list > li'
);
expect(newTodo).toBeInTheDocument();
expect(newTodo.innerHTML);
.toBe('Learn how to test');
});
Advantages
Advantages
#1: Avoid the test user
Advantages
#1: Avoid the test user
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<button
{/* Test for incrementing the count on button click */}
onClick={() => setCount((count) => count + 1)}
{/* Test for disabling the button once the count gets to 3 */}
className={count >= 3 ? "disabled" : ""}
>
Increment
</button>
<div>Count: {count}</div>
</>
);
}
Advantages
#1: Avoid the test user
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<button
{/* Test for incrementing the count on button click */}
onClick={() => setCount((count) => count + 1)}
{/* Test for disabling the button once the count gets to 3 */}
className={count >= 3 ? "disabled" : ""}
>
Increment
</button>
<div>Count: {count}</div>
</>
);
}
function Counter() {
const [clicked, setClicked] = useState(0);
return (
<>
<button
onClick={() => setCount((clicked) => clicked + 1)}
disabled={click >= 3}
>
Increment
</button>
<div>Count: {clicked}</div>
</>
);
}
Advantages
#2: Readable and meaningful test code
Advantages
#3: Encourages accessibility
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div>
<h2>{title}</h2>
<form>
<label>Full name</label>
<input />
<input type="submit" />
</form>
</div>
)
}
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div>
<h2>{title}</h2>
<form>
<label>Full name</label>
<input />
<input type="submit" />
</form>
</div>
)
}
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div role="dialog">
<h2>{title}</h2>
<form>
<label>Full name</label>
<input />
<input type="submit" />
</form>
</div>
)
}
test("renders the dialog", () => {
render(<Dialog title="iRequest" />)
expect(screen.getByRole("dialog"))
.toBeInTheDocument()
})
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div role="dialog">
<h2>{title}</h2>
<form>
<label>Full name</label>
<input />
<input type="submit" />
</form>
</div>
)
}
test("can submit the form values", () => {
render(<Dialog title="iRequest" />)
const fullNameInput = screen.getByLabelText("Full name")
// ...
})
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div role="dialog">
<h2>{title}</h2>
<form>
<label>Full name</label>
<input />
<input type="submit" />
</form>
</div>
)
}
test("can submit the form values", () => {
render(<Dialog title="iRequest" />)
const fullNameInput = screen.getByLabelText("Full name") // 🤷♂️
// 🚨 Error: Cannot find input with label text full name
// ...
})
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div role="dialog">
<h2>{title}</h2>
<form>
<label for="full-name">Full name</label>
<input id="full-name" />
<input type="submit" />
</form>
</div>
)
}
test("can submit the form values", () => {
render(<Dialog title="iRequest" />)
const fullNameInput = screen.getByLabelText("Full name") // 🤷♂️
// 🚨 Error: Cannot find input with label text full name
// ...
})
Advantages
#3: Encourages accessibility
function Dialog({ title }) {
return (
<div role="dialog">
<h2>{title}</h2>
<form>
<label for="full-name">Full name</label>
<input id="full-name" />
<input type="submit" />
</form>
</div>
)
}
test("can submit the form values", () => {
render(<Dialog title="iRequest" />)
const fullNameInput = screen.getByLabelText("Full name") // 👍
// ...
})
Advantages
#4: @testing-library is universal
Advantages
#4: @testing-library is universal
Advantages
#4: @testing-library is universal
Trade-offs
Trade-offs
#1: Tests are harder to write
Trade-offs
#1: Tests are harder to write
#1a: No more shallow rendering*
*can still stub out components with Jest, but not recommended by @testing-library
Trade-offs
#1: Tests are harder to write
#1a: No more shallow rendering*
#1b: Reliance on third-party components
*can still stub out components with Jest, but not recommended by @testing-library
Trade-offs
#1: Tests are harder to write
#1a: No more shallow rendering*
#1b: Reliance on third-party components
#1c: Thinking about UI tests in a new way
*can still stub out components with Jest, but not recommended by @testing-library
Trade-offs
#1: Tests are harder to write
#1a: No more shallow rendering*
#1b: Reliance on third-party components
#1c: Thinking about UI tests in a new way
#2: Tests are marginally slower
*can still stub out components with Jest, but not recommended by @testing-library
Trade-offs
#1: Tests are harder to write
#1a: No more shallow rendering*
#1b: Reliance on third-party components
#1c: Thinking about UI tests in a new way
#2: Tests are marginally slower
#3: Accessibility learning curve
*can still stub out components with Jest, but not recommended by @testing-library
Testimonials
Supported by React
Growing Popularity
At a high level...
At a high level...
- Write tests to increase confidence
At a high level...
- Write tests to increase confidence
The more your tests resemble the way your software is used, the more confidence they can give you.
Questions?
Testing UI Components
By Brett Abramczyk
Testing UI Components
- 71