Why are functional tests so much longer than unit tests?
Tags: testing, react
Published: 2022-06-02 23:13:53



<figcaption>Photo by <a href="https://unsplash.com/@nhphoto?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" data-href="https://unsplash.com/@nhphoto?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" class="markup--anchor markup--figure-anchor" rel="noopener" target="_blank">Nicolas Häns</a> on&nbsp;<a href="https://unsplash.com/s/photos/sprint-vs-marathon?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" data-href="https://unsplash.com/s/photos/sprint-vs-marathon?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" class="markup--anchor markup--figure-anchor" rel="noopener" target="_blank">Unsplash</a></figcaption>

---
My unit testing background has trained me to write short tests, each covering one discrete piece of code. This makes unit tests simple to read and easy to diagnose. So why can’t this approach be applied to functional tests?

---
## Functional Tests vs Unit tests

Let’s back up a bit here to distinguish between unit and functional tests. Unit tests isolate one piece of code, often a function or React component. By definition, they are tightly associated with the code, which makes them easy to diagnose (they point right to the area in the code that’s causing the failure!) but sometimes farther away from the actual user experience (especially if the coder uses mocks to keep other parts of code from polluting the unit test).

Functional tests on the other hand, test a user flow representing some functionality of your app. These emphasize interacting with the app the way a user would (clicking buttons and entering text) and de-emphasize any focus on specific code (you may have heard that it’s better not to test “internals” — or what’s going on in the code — because this makes your tests brittle and susceptible to breaking on refactors, even though there’s nothing wrong with your app).

React testing is currently trending strongly toward functional tests and away from isolated unit tests. This is great for the reasons mentioned above and in my [Enzyme vs Testing Library](https://bonniedotdev.medium.com/testing-react-a-converts-journey-from-enzyme-to-testing-library-70f85eebb674) post, but it can lead to tests that seem uncomfortably long and meandering to an old unit tester like me.

### Example of a long(ish) functional test

Say you have an app that allows people to design and order an ice cream sundae. Before they submit the order, the terms and conditions warn their ice cream order isn’t ever gonna happen (via a popover):

![Mockup showing popover text on mouseover of “Terms and Conditions”](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdwwx8xdi18zmlvju9gv.png)

<figcaption>You can’t say you weren’t&nbsp;warned</figcaption>

### The test

Here is the test for the popover, using [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) syntax. This test is short compared to a lot of my actual functional tests (hence the “ish” in the title of this section). Still I wanted something small and easy to follow for this article.

```jsx
    test('popover responds to hover', async () => {  
          render(<SummaryForm />);  
           
          // assertion 1:   
          // popover starts out hidden  
          const nullPopover = screen.queryByText(  
            /no ice cream will actually be delivered/i  
          );  
          expect(nullPopover).not.toBeInTheDocument();  
           
          // assertion 2:   
          // popover appears upon mouseover of checkbox label  
          const termsAndConditions = screen.getByText(/terms and conditions/i);  
          userEvent.hover(termsAndConditions);  
           
          const popover = screen.getByText(/no ice cream will actually be delivered/i);  
          expect(popover).toBeInTheDocument();  
           
          // assertion 3:  
          // popover disappears when we mouse out  
          userEvent.unhover(termsAndConditions);  
          await waitForElementToBeRemoved(() =>  
            screen.queryByText(/no ice cream will actually be delivered/i)  
          );  
        });
```

This test has three assertions:

1.  The popover is hidden when the component is first rendered
2.  The popover appears when the mouse hovers over “Terms and Conditions”
3.  The popover disappears when the mouse leaves “Terms and Conditions”

---
## Why not make three separate tests?

This particular test could be broken into three tests, one for each of the assertions above:

```jsx
    // test #1 //
    test('popover starts out hidden', async () => {  
      render(<SummaryForm />);
     // assertion 1  
      const nullPopover = screen.queryByText(  
        /no ice cream will actually be delivered/i  
      );  
      expect(nullPopover).not.toBeInTheDocument();  
    });

    // test #2 //
    test('popover appears after mouseover', () => {  
      render(<SummaryForm />);  
       
      // find and mouseover the Terms and Conditions text  
      const termsAndConditions = screen.getByText(/terms and conditions/i);  
      userEvent.hover(termsAndConditions);
     // assertion 2  
      popover = screen.getByText(/no ice cream will actually be delivered/i);  
      expect(popover).toBeInTheDocument();  
    });

    // test #3 //
    test('popover disappears on mouseout', () => {  
      render(<SummaryForm />);  
       
      // find and mouseover the Terms and Conditions text  
      const termsAndConditions = screen.getByText(/terms and conditions/i);  
      userEvent.hover(termsAndConditions);  
        
      // make sure the assertion appeared  
      popover = screen.getByText(/no ice cream will actually be delivered/i);  
      expect(popover).toBeInTheDocument();
     // assertion 3  
      userEvent.unhover(termsAndConditions);  
      await waitForElementToBeRemoved(() =>  
        screen.queryByText(/no ice cream will actually be delivered/i)  
      );  
    });
```

However, this is not necessarily an improvement — especially when it comes to separating tests (2) and (3). In order to set up the third test (popover disappears), we’d have to go through all the same steps we went through in the second test (getting the popover to appear, since we don’t know whether the popover disappeared unless it was actually there at some point).

Repeating the “appears” code in two separate tests feels repetitive and unnecessary.

### What about a beforeEach?

Maybe we should put the “appears” code in a [beforeEach](https://jestjs.io/docs/en/api#beforeeachfn-timeout) that runs, well, before each test.

```jsx
    describe('popover appears and disappears', () => {  
      beforeEach(() => {  
        render(<SummaryForm />);  
         
        // find and mouseover the Terms and Conditions text  
        const termsAndConditions = screen.getByText(/terms and conditions/i);  
        userEvent.hover(termsAndConditions);  
      });

    // test #1 //
    test('popover starts out hidden', async () => {  
      render(<SummaryForm />);
     // assertion 1:   
      const nullPopover = screen.queryByText(  
        /no ice cream will actually be delivered/i  
      );  
      expect(nullPopover).not.toBeInTheDocument();  
    });

    // test #2 //
    test('popover appears after mouseover', () => {  
       // assertion 2:   
       popover = screen.getByText(/no ice cream will actually be delivered/i);  
       expect(popover).toBeInTheDocument();  
     });

    // test #3 //
    test('popover disappears on mouseout', () => {  
       // assertion 3  
       userEvent.unhover(termsAndConditions);  
       await waitForElementToBeRemoved(() =>  
         screen.queryByText(/no ice cream will actually be delivered/i)  
       );  
     });  
   });
```

Here, we’re writing one test that doesn’t do anything other than assert the popover is in the document. Then there's a second test that builds on the `beforeEach` by running through the "disappears" code. That would work to break this particular test into three tests.

### Reasons NOT to use a beforeEach

1. Any time you break up the test code by using `beforeEach`, the code is less readable and requires more effort to determine what exactly happened when debugging failing tests.

2. This second reason is the stronger one for me.

For this fairly simple user flow test, one `beforeEach` would cover the setup for both of the tests that aren't for initial conditions. However, imagine a more involved user flow, where a user:

*   logs on to the site
*   selects some ice cream scoops and toppings
*   sees the order summary
*   agrees to the terms and conditions
*   sees a confirmation page

A single `beforeEach` will not be able to cover the setup for the half dozen or so actions we go through as part of the flow. If we wanted to isolate each action / assertion into its own test, it would require either

a. repeating a lot of the setup of the previous tests, or

b. mocking and/or setting the context value explicitly in order to set up the tests -- which is frowned upon in this type of functional, user-based testing because it's not what a user would actually do to get into that situation.

---
## Conclusion

The above is a verbose way of saying: most functional tests go through a series of steps, each step relying on the consequences of the previous step. The only way to run these tests without a huge amount of complexity or verbosity is to go through the whole flow in a single test, asserting along the way.

Is this isolated? No. Is this testing one specific area of the code? Also no. The point here is to test the *app*, not the *code* so that the tests more accurately reflect whether or not the app is working from a user’s perspective. The down side is that these tests are harder to debug since they’re more vague about which part of the code caused the error. This is part of the cost of functional testing. If you’re interested in how unit testing can mitigate this, take a look at my article on [When to Unit Test your React App](https://dev.to/bonnie/when-to-unit-test-your-react-app-4659).