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?

Made with Slides.com