User Driven Testing (UDT)
Tags: react, testing, ux
Published: 2022-10-11 13:33:18

> _**TL:DR**; Our integration tests should mimic our users' behaviour. Our users don't care how our features are implemented, so neither should our tests._

## What is it and why do we have a new acronym?

Everyone in tech loves a good acronym - or at least I hope so with how many the tech community have floating around! Why not add User Driven Testing (UDT) to the list? 👀

I had the idea to write this article after attending two workshops: Kateryna Porshnieva's _Comprehensive guide to testing React applications_ workshop and Sandrina Pereira's _Web Accessibility in React Apps_ workshop. I’d already been tackling the best way to structure tests for a new React application my company was working on and I kept coming to the same conclusion - our tests were just users of our application... so why don't we write our tests to behave as users?

## Why did I write this article?

> _**Note:** This blog post uses a React app as an example, but I do believe that the principles of this blog are applicable when coming to testing any application._

If you were trying to figure out how to test your React application, you would probably find yourself reading [React's documentation on testing](https://reactjs.org/docs/testing.html). You would then probably read about [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) (RTL, for the acronym aficionados). You would then probably read [RTL's Guiding Principles](https://testing-library.com/docs/guiding-principles). And you would then come across a tweet that makes so much sense that you don't know why you didn't think of it before.

{% embed https://twitter.com/kentcdodds/status/977018512689455106 %}

At the time, I was setting up a new React app for my company. I now knew _what_ I wanted to achieve: a test suite that mimicked how our users would interact with our app, but it wasn't obvious to me _how_ I could achieve it.

As our app evolved, components were refactored, business logic kept being moved around, and, as a results, our tests were often being rewritten.

"But hang on... When you were doing all these refactors, did the users' behaviour with the application change?" Nope. The users still have the same experience... So why did we have to rewrite our tests?

## What was the 'eureka!' moment?

If you're not familiar with RTL, RTL renders the component and all its dependencies to mimic the DOM tree rendered by the browser. That basically means that if a bug is introduced in a dependency of a component being tested, there is a chance the components tests will fail. I know what you’re thinking... "Aleo, if RTL renders a component's children, wouldn't that make the tests... _integration_ tests?" Yes, it would! And that's when it got me thinking - we want our tests to pass if the feature in our app is working, we don’t really care which component is responsible for our feature, do we? So the solution is simple - we want to render our `App` component in all our tests! Right?

Well... no. If we render our `App` component, we could have to complete prerequisite tasks repeatedly (like navigating to the page the feature is on) before we even start testing the feature in question. Well why can't we come to a middle ground and just render the component that represents our page?

## Bringing theory to practice

We've spoken a lot about the theory, but theory can only go so far - let's have a look at an example (you can find the source code on my GitHub [here](https://github.com/aleoyakas/user-driven-testing)).

For this example, we've been asked to update our company's ticket booking app. When the app was built, the company was only running holding one event at a time. The company now wants to run multiple events and users should be able to book tickets onto all of them. We've been tasked with refactoring the code to allow users to book on as many events as they would like.

### What you're starting with

The (oversimplified) app is composed of two components:
- App
- Event

The `App` component looks like so:

```react
import React, { FC } from 'react';
import Event from '../Event';
import styles from './App.module.css';

const App: FC = () => {
  return (
    <div className={styles.container}>
      <h1>User Driven Testing</h1>
      <Event />
    </div>
  );
};

export default App;
```

and the `Event` component looks like so:

```react
import React, { FC, useState } from 'react';
import purchaseTickets from '../../requests/purchaseTickets';
import joinClasses from '../../utils/joinClasses';
import styles from './Event.module.css';

const Event: FC = () => {
  const [ticketCount, setTicketCount] = useState(0);

  const increaseTicketCount = () =>
    setTicketCount((currentValue) => currentValue + 1);

  const decreaseTicketCount = () => {
    if (ticketCount > 0) {
      setTicketCount((currentValue) => currentValue - 1);
    };
  };

  const resetTicketCount = () =>
    setTicketCount(0);

  return (
    <div className={styles.container}>
      <h2>Super cool event!</h2>
      <div className={styles.btnGroup}>
        <button 
          onClick={decreaseTicketCount}
          className={joinClasses([styles.btn, styles.btnSecondary])}
        >
          Remove
        </button>
        <p>{ticketCount}</p>
        <button
          onClick={increaseTicketCount}
          className={joinClasses([styles.btn, styles.btnSecondary])}
        >
          Add
        </button>
      </div>
      <div className={styles.btnGroup}>
        <button
          onClick={resetTicketCount}
          className={joinClasses([styles.btn, styles.btnSecondary])}
          aria-disabled={!ticketCount}
        >
          Empty basket
        </button>
        <button
          onClick={() => purchaseTickets(ticketCount)}
          className={joinClasses([styles.btn, styles.btnPrimary])}
        >
          Purchase
        </button>
      </div>
    </div>
  );
};

export default Event;
```

The first thing you notice is that the "Purchase" and "Empty" checkout buttons are in the `Event` component. You're aware you're going to have to refactor the code to allow you to add more events.

Before you start refactoring the code, you decide to look at the tests; the `App` test are minimal at best:

```react
import { render, screen } from '@testing-library/react';
import App from '.';

describe('App', () => {
  test('App loads correctly', () => {
    const { container } = render(<App />);

    expect(screen.getByRole('heading', { name: /User Driven Testing/i })).toBeVisible();
    expect(container.firstChild.children.length).toBe(2);
  });
});
```

but the `Event` tests look quite detailed:

```react
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import Event from '.';
import purchaseTickets from '../../requests/purchaseTickets';

jest.mock('../../requests/purchaseTickets');
const purchaseTicketsMock = purchaseTickets as jest.Mock;

beforeEach(() => {
  purchaseTicketsMock.mockReset();
});

describe('Event', () => {
  test('Event loads correctly', () => {
    render(<Event />);

    expect(screen.getByRole('heading', { name: /Super cool event!/i })).toBeVisible();
    expect(screen.getByRole('button', { name: /Remove/i })).toBeVisible();
    expect(screen.getByRole('button', { name: /Add/i })).toBeVisible();
    expect(screen.getByRole('button', { name: /Empty basket/i })).toBeVisible();
    expect(screen.getByRole('button', { name: /Purchase/i })).toBeVisible();
  });

  test('tickets can be added', () => {
    render(<Event />);

    const addBtn = screen.getByRole('button', { name: /Add/i });
    const ticketTotal = screen.getByText('0');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('1');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('2');
  });

  test('tickets can be reduced down to 0', () => {
    render(<Event />);

    const addBtn = screen.getByRole('button', { name: /Add/i });
    const removeBtn = screen.getByRole('button', { name: /Remove/i });
    const ticketTotal = screen.getByText('0');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('1');

    user.click(removeBtn);
    expect(ticketTotal).toHaveTextContent('0');

    user.click(removeBtn);
    expect(ticketTotal).toHaveTextContent('0');
  });

  test('empty basket button sets ticket count to 0', () => {
    render(<Event />);

    const addBtn = screen.getByRole('button', { name: /Add/i });
    const emptyBasketBtn = screen.getByRole('button', { name: /Empty basket/i });
    const ticketTotal = screen.getByText('0');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('1');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('2');

    user.click(emptyBasketBtn);
    expect(ticketTotal).toHaveTextContent('0');
  });

  test('purchase button calls purchaseTickets', () => {
    render(<Event />);

    const addBtn = screen.getByRole('button', { name: /Add/i });
    const purchaseBtn = screen.getByRole('button', { name: /Purchase/i });
    const ticketTotal = screen.getByText('0');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('1');

    user.click(addBtn);
    expect(ticketTotal).toHaveTextContent('2');

    user.click(purchaseBtn);
    expect(purchaseTicketsMock).toHaveBeenCalledWith(2);
  });
});
```

### Time for a refactor

You decide to create an `EventsGroup` component which will be responsible for rendering all your `Event` components and rendering your "Purchase" and "Empty" buttons. Your `EventsGroup` component ends up looking something like this:

```react
import React, { FC, useState } from 'react';
import purchaseTickets from '../../requests/purchaseTickets';
import Button from '../../components/Button';
import Event from '../../components/Event';
import styles from './EventsGroup.module.css';

const EventsGroup: FC = () => {
  const ticketCountInit = {
    superCoolEvent: 0,
    anotherAmazingEvent: 0,
  }
  const [ticketCount, setTicketCount] = useState<Record<string, number>>(ticketCountInit);
  
  const totalTicketCount = Object.values(ticketCount)
    .reduce((total, current) => total + current, 0);

  const increaseTicketCount = (eventKey: string) => () =>
  setTicketCount((currentValue) => ({
    ...currentValue,
    [eventKey]: currentValue[eventKey] + 1,
  }));

  const decreaseTicketCount = (eventKey: string) => () => {
    if (ticketCount[eventKey] > 0) {
      setTicketCount((currentValue) => ({
        ...currentValue,
        [eventKey]: currentValue[eventKey] - 1,
      }));
    };
  };

  const resetTicketCount = () =>
    setTicketCount(ticketCountInit);

  return (
    <div className={styles.container}>
      <Event
        eventName="Super Cool Event!"
        ticketCount={ticketCount.superCoolEvent}
        increaseTicketCount={increaseTicketCount('superCoolEvent')}
        decreaseTicketCount={decreaseTicketCount('superCoolEvent')}
      />
      
      <div className={styles.btnGroup}>
        <Button
          onClick={resetTicketCount}
          isAriaDisabled={totalTicketCount <= 0}
          variant="secondary"
        >
          Empty basket
        </Button>
        <Button
          onClick={() => purchaseTickets(totalTicketCount)}
          variant="primary"
        >
          Purchase
        </Button>
      </div>
    </div>
  );
};

export default EventsGroup;
```

And your `Event` component ends up looking like this:

```react
import React, { FC } from 'react';
import Button from '../Button';
import styles from './Event.module.css';

interface Props {
  eventName: string
  ticketCount: number
  increaseTicketCount: () => void
  decreaseTicketCount: () => void
};

const Event: FC<Props> = ({
  eventName,
  ticketCount,
  increaseTicketCount,
  decreaseTicketCount
}) => {
  return (
    <div className={styles.container}>
      <h2>{eventName}</h2>
      <div className={styles.btnGroup}>
        <Button 
          onClick={decreaseTicketCount}
          variant="secondary"
          isAriaDisabled={ticketCount <= 0}
        >
          Remove
        </Button>
        <p>{ticketCount}</p>
        <Button
          onClick={increaseTicketCount}
          variant="secondary"
        >
          Add
        </Button>
      </div>
    </div>
  );
};

export default Event;
```

Amazing! You've refactored your code and it's functionally the same! It looks the same in the browser, you can still add/remove tickets exactly the same, and we can easily add as many events as we like in future! So as far as you're concerned, you're job is done! Right?

Well... we've forgotten an important step: to run our tests. We may have forgotten, but our pipeline hasn't - and our pipeline is _not_ happy. All our tests are failing. But why? The code is functionally the same?

### But why didn't our tests pass?

> _"I would have gotten away with it too, if it weren't for you meddling tests!" ~some Scooby Doo antagonist_

So our tests aren't passing, everyone in our team has seen, and we're extra annoyed because the app works _exactly_ the same. How can we fix this? Or better yet, how could we have _avoided_ this?

If we want to fix this, there is a simple solution of updating our unit tests. But should our tests really be breaking if the app works the same?

An alternative (and, by me writing this article, my obvious preferred approach) is to convert our _unit_ tests to _integration_ tests. These integration tests will allow us to verify app _functions_ the way we expect and will allow us to refactor our code in future without risk of breaking our tests (ya know, provided we haven't _actually_ broken anything...).

To do this, we can use the test cases defined in our `Event` test file - but instead of rendering our `Event` component, we can render our `App` container. The tests will all pass now! Yay!

### Are these really integration tests?

Now I know what you're thinking, "surely that's just an end-to-end test?". Well, while we are testing the all our code in one test file, we should really be seeing it as only testing one feature of our code. In larger code bases, I would **not** recommend rendering the `App` component in your integration tests - instead, I would recommend rendering the lowest level container responsible for the _feature_ you're testing (in this case, we actually could render `EventsGroup` instead, but we didn't have this component at the start).

### Location, location, location

Where we store our tests in all down to personal preference. I prefer to keep my integration tests outside my `src` folder, some people like to have them within the `src` folder, and others prefer to have them next to the component they are rendering in the test.

It really is up to you, but I prefer to keep my `src` folder for functional code and unit tests (yes, I do think we should have unit tests on occasion too).

## Conclusion

So we set out to refactor a our company's ticket app but broke our tests because they weren't written from a user's perspective. We've seen that if we had written our tests from a user's perspective, we could have made our refactor and our tests would have confidently told us we haven't broken any features of our app. 

I do think it's important to still write unit tests for core pieces of logical code (say, any bespoke hooks you write), but the majority of our testing should be done on an integration level for the most return on investment.

Our primary care is how our users interacts with our app. Our users don't care how our features are implemented, so neither should our tests.