Skip to content

Commit

Permalink
more testing
Browse files Browse the repository at this point in the history
  • Loading branch information
btholt committed Oct 17, 2024
1 parent baf3b0c commit 7fedf2b
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 39 deletions.
45 changes: 14 additions & 31 deletions lessons/07-testing/A-vitest.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,13 @@ They designed it to be a drop-in replacement for [Jest][jest] which is what I ha
While Vitest is not using Jasmine directly, its APIs mimic Jasmine APIs (just like Jest.)

Let's get going Run `npm install -D [email protected] @vitest/[email protected] [email protected] [email protected]`.
Let's get going. Run `npm install -D [email protected] @testing-library/[email protected] [email protected]`.

> This course previously taught [@testing-library/react][tlr] and used a synthetic DOM tool called [happy-dom][hd]. Vitest now has a first class integration with [Playwright][playwright] and their own version @testing-library/react, vitest-browser-react. Hence this version will use those tools. If you want to learn those other tools, the [v8][v8] version of this course all still works.
`@testing-library/react`, formerly called `react-testing-library`, is a tool that has a bunch of convenience features that make testing React significantly easier and is now the recommended way of testing React, supplanting [Enzyme][enzyme]. Previous versions of this course teach Enzyme if you'd like to see that (though I wouldn't recommend it unless you have to.)

We are going to be using the latest-and-greatest testing tool from Microsoft called Playwright, the spirtual successor to Google's Puppeteer tool. Using this tool, your command line client will spin up a _real_ copy of Webkit, Firefox, or Chromium and run your tests inside of them. Can't beat the real thing, and it's finally fast and not-flaky enough for it to be worth it!
We need to tell Vitest that we need a browser-like environment which it will fulfill via the [happy-dom][hd] package. happy-dom is a lot like jsdom but smaller, doesn't do 100% of what the browser does, and is much, much faster.

Okay, let's go set up Vitest to work with our Vite project. In your `vite.config.js`

```javascript

// under server
test: {
setupFiles: ["vitest-browser-react"],
browser: {
enabled: true,
name: "firefox", // or chromium or webkit
provider: "playwright",
},
},
```

If you've never run Playwright before, you should run `npx playwright install` to install all the browser stuff necessary.
Next go into your src directory and create a folder called `__tests__`. Notice that's double underscores on both sides. Why double? They borrowed it from Python where double underscores ("dunders" as I've heard them called) mean something magic happens (in essence it means the name itself has significance and something is looking for that path name exactly.) In this case, Vitest assumes all JS files in here are tests.

Let's go add an npm script. In your package.json.

Expand All @@ -45,28 +30,26 @@ Let's go add an npm script. In your package.json.

> Fun trick: if you call it test, npm lets you run that command as just `npm t`.
This command let's you run Vitest in an interactive mode where it will re-run tests selectively as you save them. This lets you get instant feedback if your test is working or not. This is probably my favorite feature of Vitest. So try it now. It should error because we have no tests, but it should open a browser with the testing harness loaded.
This command let's you run Jest in an interactive mode where it will re-run tests selectively as you save them. This lets you get instant feedback if your test is working or not. This is probably my favorite feature of Vitest.

```bash
# these all do the same thing
npm run test
npm test
npm t
```
Okay, one little configuration to add to your vite.config.js

Also good to go install the [Vitest VS Code extension][vitest-vsc] if you're using VS Code.
```javascript
// add this to the config object
test: {
environment: "happy-dom",
},
```

Now that we've got that going, let's go write a test.

[vitest-vsc]: https://marketplace.visualstudio.com/items?itemName=vitest.explorer
[jest]: https://jestjs.io
[jasmine]: https://jasmine.github.io/
[enzyme]: http://airbnb.io/enzyme/
[istanbul]: https://istanbul.js.org
[res]: https://raw.githubusercontent.com/btholt/complete-intro-to-react-v5/testing/__mocks__/@frontendmasters/res.json
[app]: https://github.com/btholt/citr-v8-project/tree/master/14-context
[fb]: https://twitter.com/cpojer/status/1524419433938046977
[hd]: https://github.com/capricorn86/happy-dom
[vitest]: https://vitest.dev/
[v4]: https://frontendmasters.com/courses/intermediate-react-v4/setup-jest-testing-library/
[tlr]: https://github.com/testing-library/react-testing-library
[v8]: https://frontendmasters.com/courses/intermediate-react-v5/setup-react-testing-library-vitest/
[playwright]: https://playwright.dev/
71 changes: 71 additions & 0 deletions lessons/07-testing/B-basic-react-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
description: ""
---

Let's write our first test for Pizza.jsx. In general, here's my methodology for testing React:

- Try to test functionality, not implementation. Make your tests interact with components as a user would, not as a developer would. This means you're trying to do more to think of things like "what would a user see" or "if a user clicks a button a modal comes up" rather than "make sure this state is correct" or "ensure this library is called". This isn't a rule; sometimes you need to test those things too for assurance the app is working correctly. Use your best judgment.
- Every UI I've ever worked on changes a lot. Try to not unnecessarily spin your wheels on things that aren't important and are likely to change.
- In general when I encounter a bug that is important for me to go back and fix, I'll write a test that would have caught that bug. Actually what I'll do is _before_ I fix it, I'll write the test that fails. That way I fix it I'll know I won't regress back there.
- Ask yourself what's important about your app and spend your time testing that. Ask yourself "if a user couldn't do X then the app is worthless" sort of questions and test those more thoroughly. If a user can't change themes then it's probably not the end of the world (a11y is important) so you can spend less time testing that but if a user can't log in then the app is worthless. Test that.
- Delete tests on a regular basis. Tests have a shelf life.
- Fix or delete flaky tests. Bad tests are worse than no tests

Okay, create a new file called `Pizza.test.jsx`. This naming convention is just habit. `Pizza.spec.jsx` is common too. But as long as it's in the `__tests__` directory it doesn't much matter what you call it.

```javascript
import { render } from "@testing-library/react";
import { expect, test } from "vitest";
import Pizza from "../Pizza";

test("alt text renders on image", async () => {
const name = "My Favorite Pizza";
const src = "https://picsum.photos/200";
const screen = render(
<Pizza name={name} description="super cool pizza" image={src} />
);

const img = screen.getByRole("img");
expect(img.src).toBe(src);
expect(img.alt).toBe(name);
});
```

This is your most basic test. It renders a React component, and then starts running assertions on it. Here we're asserting that our pizza image ends up with the correct alt text on it. Not the most useful test but a good start for us seeing how to assert something. I could see this being useful if we had bugs that occasionally alt text wasn't showing up.

We're using getByRole here to grab the image on the page. In general React Testing Library wants you to adopt a user-centric mindset of how you're asserting things on the page. In thise case we're trying to find an image which is something a user would see. Contrast that with using a CSS selector to select the image (which you can do, it's just frowned upon) which is very implementation-centric (a user doesn't know nor care about CSS classes.)

Let's add another test to make sure that we have a default image if pizza isn't passed an image.

> 🚨 This doesn't work yet. That's intentional.
```javascript
test("to have default image if none is provided", async () => {
const screen = render(
<Pizza name={"Cool Pizza"} description="super cool pizza" />
);

const img = screen.getByRole("img");
expect(img.src).not.toBe("");
});
```

Well, first thing it is going to complain about is that we don't clean up after ourselves after each test. React Testing Library requires us to.

```javascript
// at top
import { render, cleanup } from "@testing-library/react"; // add cleanup
import { afterEach, expect, test } from "vitest"; // add afterEach
afterEach(cleanup);
```

The test still doesn't pass? Oh, that's because it caught a bug! If you don't give it a image, it just breaks. That's not a good user experience. Let's go fix it in Pizza.jsx.

```javascript
// replace src
src={props.image ? props.image : "https://picsum.photos/200"}
```

Now it works!

Bam! Some easy React testing there for you.
71 changes: 71 additions & 0 deletions lessons/07-testing/C-testing-user-interaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
Let's test the Contact page. It has a nice, simple user interaction. User puts their information in, we submit to the API, and we show them a submitted header. Let's test all of that.

First we need a helper function, vitest-fetch-mock. We don't _need_ it because Vitest can do it, but I think it just makes things easier.

```bash
npm i -D [email protected]
```

And now let's write the code

```javascript
import { render } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { Route } from "../routes/contact.lazy";

const queryClient = new QueryClient();

const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();

test("can submit contact form", async () => {
fetchMocker.mockResponse(JSON.stringify({ status: "ok" }));
const screen = render(
<QueryClientProvider client={queryClient}>
<Route.options.component />
</QueryClientProvider>
);

const nameInput = screen.getByPlaceholderText("Name");
const emailInput = screen.getByPlaceholderText("Email");
const msgTextArea = screen.getByPlaceholderText("Message");

const testData = {
name: "Brian",
email: "[email protected]",
message: "This is a test message",
};

nameInput.value = testData.name;
emailInput.value = testData.email;
msgTextArea.value = testData.message;

const btn = screen.getByRole("button");

btn.click();

const h3 = await screen.findByRole("heading", { level: 3 });

expect(h3.innerText).toContain("Submitted");

const requests = fetchMocker.requests();
expect(requests.length).toBe(1);
expect(requests[0].url).toBe("/api/contact");
expect(fetchMocker).toHaveBeenCalledWith("/api/contact", {
body: JSON.stringify(testData),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
});
```

- We need to wrap React Query with a query provider always or it doesn't work. In this case we can just make that part of the initial render.
- You can pre-populate React Query with a valid cache and then test if your app uses that correctly. Totally valid way to test. In this case we want to make sure our endpoint is being posted to correctly (to make sure we record a contact)so we aren't doing that.
- `vi` is Vitest's spy library, similar to sinon.
- vitest-fetch-mock is just a nice layer on top of vi. We could just use vi directly.
- Notice we can just call `click` and it does all the things we'd expect. Cool, right?
- We then make some assertions that our h3 shows up correctly, that our API was called once, and that it was submitted to our API the way we expect it to. This may seem a bit implementation-oriented, and it is, but typically in a larger app we'd have a library for calling our API and we'd test that individually but we don't here so we're wrapping it all together.
74 changes: 74 additions & 0 deletions lessons/07-testing/E-vitest-browser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
description: ""
---

# TODO – this is to be rewritten

This is meant to be a very brief treatise on how to do testing on React applications. This will be a brief intro on how to set up Vitest tests for the application we just created.

## Testing with Vitest

[Vitest][vitest] is a test runner made by the fine folks who make Vite (as well as Vue.) The idea behind Vitest is that you already have a complete build pipeline for making an app, why should that pipeline be any different for test? It shouldn't; you want your testing environment to look as much like your app environment as possible.

They designed it to be a drop-in replacement for [Jest][jest] which is what I have taught for this course since the beginning. Jest is great and still a very viable tool to use for testing, even with Vite. We're just going to use Vitest because 1. we don't have to do any more configuration and 2. 100% of what you will learn in here is going to be useful if you use Jest. Win-win. If you want to learn Jest specifically, [take a look at Intermediate React v4's testing section.][v4]

> Fun side note: [Jest is now an OpenJS project and no longer directly under Facebook][fb].
While Vitest is not using Jasmine directly, its APIs mimic Jasmine APIs (just like Jest.)

Let's get going Run `npm install -D [email protected] @vitest/[email protected] [email protected] [email protected]`.

> This course previously taught [@testing-library/react][tlr] and used a synthetic DOM tool called [happy-dom][hd]. Vitest now has a first class integration with [Playwright][playwright] and their own version @testing-library/react, vitest-browser-react. Hence this version will use those tools. If you want to learn those other tools, the [v8][v8] version of this course all still works.
We are going to be using the latest-and-greatest testing tool from Microsoft called Playwright, the spirtual successor to Google's Puppeteer tool. Using this tool, your command line client will spin up a _real_ copy of Webkit, Firefox, or Chromium and run your tests inside of them. Can't beat the real thing, and it's finally fast and not-flaky enough for it to be worth it!

Okay, let's go set up Vitest to work with our Vite project. In your `vite.config.js`

```javascript

// under server
test: {
setupFiles: ["vitest-browser-react"],
browser: {
enabled: true,
name: "firefox", // or chromium or webkit
provider: "playwright",
},
},
```

If you've never run Playwright before, you should run `npx playwright install` to install all the browser stuff necessary.

Let's go add an npm script. In your package.json.

```json
"test": "vitest"
```

> Fun trick: if you call it test, npm lets you run that command as just `npm t`.
This command let's you run Vitest in an interactive mode where it will re-run tests selectively as you save them. This lets you get instant feedback if your test is working or not. This is probably my favorite feature of Vitest. So try it now. It should error because we have no tests, but it should open a browser with the testing harness loaded.

```bash
# these all do the same thing
npm run test
npm test
npm t
```

Also good to go install the [Vitest VS Code extension][vitest-vsc] if you're using VS Code.

Now that we've got that going, let's go write a test.

[vitest-vsc]: https://marketplace.visualstudio.com/items?itemName=vitest.explorer
[jest]: https://jestjs.io
[jasmine]: https://jasmine.github.io/
[enzyme]: http://airbnb.io/enzyme/
[res]: https://raw.githubusercontent.com/btholt/complete-intro-to-react-v5/testing/__mocks__/@frontendmasters/res.json
[fb]: https://twitter.com/cpojer/status/1524419433938046977
[hd]: https://github.com/capricorn86/happy-dom
[vitest]: https://vitest.dev/
[v4]: https://frontendmasters.com/courses/intermediate-react-v4/setup-jest-testing-library/
[tlr]: https://github.com/testing-library/react-testing-library
[v8]: https://frontendmasters.com/courses/intermediate-react-v5/setup-react-testing-library-vitest/
[playwright]: https://playwright.dev/
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
Before we get too far into this, here's my thoughts on testing

- Try to test functionality, not implementation. Make your tests interact with components as a user would, not as a developer would. This means you're trying to do more to think of things like "what would a user see" or "if a user clicks a button a modal comes up" rather than "make sure this state is correct" or "ensure this library is called". This isn't a rule; sometimes you need to test those things too for assurance the app is working correctly. Use your best judgment.
- Every UI I've ever worked on changes a lot. Try to not unnecessarily spin your wheels on things that aren't important and are likely to change.
- In general when I encounter a bug that is important for me to go back and fix, I'll write a test that would have caught that bug. Actually what I'll do is before I fix it, I'll write the test that fails. That way I fix it I'll know I won't regress back there.
- Ask yourself what's important about your app and spend your time testing that. Ask yourself "if a user couldn't do X then the app is worthless" sort of questions and test those more thoroughly. If a user can't change themes then it's probably not the end of the world (a11y is important) so you can spend less time testing that but if a user can't log in then the app is worthless. Test that.
- Delete tests on a regular basis. Tests have a shelf life.
- Fix or delete flaky tests. Bad tests are worse than no tests
# TODO – this is to be rewritten

Let's write a very basic test. We are going to test Pizza.jsx. Let's fathom we've had issues with the correct alt text being rendered and we want to write a test that test that our image gets rendered, with the correct src and the correct alt text.

Expand Down

0 comments on commit 7fedf2b

Please sign in to comment.