|
| 1 | +--- |
| 2 | +title: Testing a React Application What is Vitest? |
| 3 | +date: 2022-05-02 |
| 4 | +published: true |
| 5 | +description: The third part of a series of articles on how to test a React Application. I go over how Vitest and its syntax work in the simplest terms. |
| 6 | +categories: |
| 7 | + - testing |
| 8 | + - react |
| 9 | +cover_image: https://cdn.hashnode.com/res/hashnode/image/upload/v1651098470347/9coA_akWr.png |
| 10 | +--- |
| 11 | + |
| 12 | +### Introduction |
| 13 | + |
| 14 | +Unit tests are essential to ensuring confidence in the code we have written. In the fourth part of this series, I'll be going over how to write our first unit test and the thought process of how I decided what to test. |
| 15 | + |
| 16 | +[First part](https://relatablecode.com/testing-a-react-application-the-modern-approach/) |
| 17 | + |
| 18 | +[Second part](https://relatablecode.com/testing-a-react-application-the-setup/) |
| 19 | + |
| 20 | +[Third part](https://relatablecode.com/testing-a-react-application-what-is-vitest/) |
| 21 | + |
| 22 | +> A unit test is a testing method for an isolated and individual piece of code, a unit. In the context of React, this more than likely refers to testing our components in isolation and any associated function |
| 23 | +
|
| 24 | +That's great! But how do we decide exactly what to test? Our tests should only be concerned with the interactions the user expects. Implementation details such as variable names, function names, etc should all be irrelevant in our tests. |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +### Deciding on the Unit Test |
| 29 | + |
| 30 | +To give a brief review, we created a very barebones application that fetches some generic JSON and displays them on the page: |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +What we can reasonably assume the user will care about: |
| 35 | + |
| 36 | +1. When clicking on the fetch posts button it should list out the posts. |
| 37 | +2. When clicking on the clear posts button it should clear the posts. |
| 38 | + |
| 39 | +What we don't care about: |
| 40 | + |
| 41 | +1. The name of the function calling the fetch request |
| 42 | +2. The class names of the elements in the content |
| 43 | + |
| 44 | +#### Looking at our code |
| 45 | + |
| 46 | +Let's compare this by looking at our code. |
| 47 | + |
| 48 | +```js |
| 49 | +<section className="App-buttons"> |
| 50 | + <button onClick={fetchPosts} type="button"> |
| 51 | + Fetch Posts |
| 52 | + </button> |
| 53 | + <button onClick={clearPosts} type="button"> |
| 54 | + Clear posts |
| 55 | + </button> |
| 56 | +</section> |
| 57 | +{loading && <p>Loading...</p>} |
| 58 | +{posts.map((post) => ( |
| 59 | + <article key={post.id}> |
| 60 | + <h3>{post.title}</h3> |
| 61 | + <p>{post.body}</p> |
| 62 | + </article> |
| 63 | + ))} |
| 64 | +</main> |
| 65 | +``` |
| 66 | + |
| 67 | +So to visualize this as if we were the end-user: |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +What's wrong here? Well, we're testing several implementation details such as the content of the response and whether or not the function was executed. |
| 72 | + |
| 73 | +These parts should be a black box for our tests. |
| 74 | + |
| 75 | +Something better would be: |
| 76 | + |
| 77 | + |
| 78 | + |
| 79 | +You may be asking, well doesn't the first option test for more? |
| 80 | + |
| 81 | +This is an incorrect way to approach it. Code changes, we tend to refactor. If our tests are constantly breaking when making code changes we add a lot of overhead to development. |
| 82 | + |
| 83 | +In the end, what we care about is that the posts are **fetched and displayed**. The details of the function are _irrelevant_. |
| 84 | + |
| 85 | +### Modifying our App for the first Unit Test |
| 86 | + |
| 87 | +Let's modify our `vite.config.js` file: |
| 88 | + |
| 89 | +```js |
| 90 | +import react from '@vitejs/plugin-react'; |
| 91 | + |
| 92 | +// https://vitejs.dev/config/ |
| 93 | +export default defineConfig({ |
| 94 | + plugins: [react()], |
| 95 | + test: { |
| 96 | + globals: true, |
| 97 | + environment: 'jsdom', |
| 98 | + }, |
| 99 | +}); |
| 100 | +``` |
| 101 | + |
| 102 | +### Writing the Unit Test |
| 103 | + |
| 104 | +Let's go ahead and create our first test. At the root of our project let's create `App.test.js` |
| 105 | + |
| 106 | +```js |
| 107 | +import { describe } from 'vitest'; |
| 108 | + |
| 109 | +describe('Testing our React application', () => { |
| 110 | + it('Fetch posts', () => {}); |
| 111 | +}); |
| 112 | +``` |
| 113 | + |
| 114 | +But wait! While creating our test let's watch and see if they're passing or failing. In the terminal run the following command: |
| 115 | + |
| 116 | +```bash |
| 117 | +vitest --watch |
| 118 | +``` |
| 119 | + |
| 120 | +This should generate a failure as we have no assertions in our test: |
| 121 | + |
| 122 | + |
| 123 | + |
| 124 | +Next, in order to render our components, we need the help of another library: [React-testing-library](https://testing-library.com/docs/react-testing-library/intro/). |
| 125 | + |
| 126 | +> The @testing-library family of packages helps you test UI components in a user-centric way. |
| 127 | +
|
| 128 | +```bash |
| 129 | +npm install @testing-library/react @testing-library/jest-dom @testing-library/user-events --save-dev |
| 130 | +``` |
| 131 | + |
| 132 | +First, let's just check we can correctly render and pass a test: |
| 133 | + |
| 134 | +```js |
| 135 | +import React from 'react'; |
| 136 | +import { describe, expect, it } from 'vitest'; |
| 137 | +import { render, screen } from '@testing-library/react'; |
| 138 | +import '@testing-library/jest-dom'; |
| 139 | +import App from './App'; |
| 140 | + |
| 141 | +describe('Testing our React application', () => { |
| 142 | + it('Fetch posts', async () => { |
| 143 | + render(<App />); |
| 144 | + |
| 145 | + expect(screen.getByText(/Modern React Testing/i)).toBeInTheDocument(); |
| 146 | + }); |
| 147 | +}); |
| 148 | +``` |
| 149 | + |
| 150 | +Here we just render our app and check for the title of our heading. Expect in this case is our assertion that decides if we pass a test or not. |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | +### Unit Test for fetching |
| 155 | + |
| 156 | +But this isn't really relevant to actual testing. So let's try working with our button and post-fetching functionalities. |
| 157 | + |
| 158 | +```js |
| 159 | +import React from 'react'; |
| 160 | +import { describe } from 'vitest'; |
| 161 | +import { render, screen } from '@testing-library/react'; |
| 162 | +import userEvent from '@testing-library/user-event'; |
| 163 | +import '@testing-library/jest-dom'; |
| 164 | +import App from './App'; |
| 165 | + |
| 166 | +describe('Testing our React application', () => { |
| 167 | + it('Fetch posts', () => { |
| 168 | + const user = userEvent.setup(); |
| 169 | + |
| 170 | + render(<App />); |
| 171 | + |
| 172 | + expect(screen.getByText(/Modern React Testing/i)).toBeInTheDocument(); |
| 173 | + }); |
| 174 | +}); |
| 175 | +``` |
| 176 | + |
| 177 | +`userEvent` in this case lets us follow a core principle we laid out at the very beginning: Make tests that can most closely resemble how the user interacts with the application. |
| 178 | + |
| 179 | +For example, inside of our userEvent object, we have access to the click function! And with this click function, we can send in an argument to look for our button. |
| 180 | + |
| 181 | +```js |
| 182 | +userEvent.click(screen.getByRole('button', { name: 'Fetch Posts' })); |
| 183 | +``` |
| 184 | + |
| 185 | +Let's explain this with a diagram: |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | +A whole lot of utilities to click the button in our unit test. However, the function invoked by clicking the button is asynchronous. So let's make our test asynchronous and wait for the posts to be fetched. |
| 190 | + |
| 191 | +Later on, we'll mock this request to test for more possibilities. |
| 192 | + |
| 193 | +```js |
| 194 | +import { describe } from 'vitest'; |
| 195 | +import { render, screen } from '@testing-library/react'; |
| 196 | +import { userEvent } from '@testing-library/user-event'; |
| 197 | +import App from './App'; |
| 198 | + |
| 199 | +describe('Testing our React application', async () => { |
| 200 | + it('Fetch posts', () => { |
| 201 | + render(<App />); |
| 202 | + userEvent.click(screen.getByRole('button', { name:'Fetch Posts'})); |
| 203 | + |
| 204 | + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...') |
| 205 | +); |
| 206 | + |
| 207 | + expect(screen.getByRole('heading', { level: 3 })).toBeDefined(); |
| 208 | + }); |
| 209 | +}); |
| 210 | +``` |
| 211 | + |
| 212 | +Perfect. Yet again we're using the screen functionality and just waiting for the Loading text to disappear. |
| 213 | + |
| 214 | +**BUT**, this will give us an error...but why? Well, we're not stubbing or mocking the fetch service that's called when clicking the button. How can we solve that? |
| 215 | + |
| 216 | +### Wrapping it up |
| 217 | + |
| 218 | +In the next article, I'll go over how to use MSW to mock requests that are launched from the tests. We'll go over the setup and integrate it with this test! |
| 219 | + |
| 220 | +More content at [Relatable Code](https://relatablecode.com) |
| 221 | + |
| 222 | +### Let's connect |
| 223 | + |
| 224 | +If you liked this feel free to connect with me on [LinkedIn](https://www.linkedin.com/in/relatablecode) or [Twitter](https://twitter.com/relatablecoder) |
| 225 | + |
| 226 | +Check out my free developer roadmap and weekly tech industry news in my [newsletter](https://relatablecode.substack.com/). |
0 commit comments