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 metrics
  • It'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