Components People Test

How to test individual React components the same way as end-to-end web applications.

Long long time ago (aka in August of 2020) one curious reader named Guillermo Rauch in a moment of contemplation asked on Twitter what books people (re)read:

What books people love to read question

This short question has prompted lots of responses. People on Twitter are really helpful, aren't they? Then Guillermo has collected the feedback in one single Notion table with book links, cover images, and number of votes for every book. How do I know about it? Because he has blogged about it on his blog!

The produced blog post

That blog is open source and available at github.com/rauchg/blog. It is a great example of an application based on Next.js React framework. We can look at the source for the post - it is fetching the reading list data, then renders a number of Book components on the page:

/pages/2020/books-people-reread.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import {
loadPageChunk,
queryCollection,
getCollectionSchemaNameIndex
} from '../../lib/notion'

export async function getStaticProps() {
const { recordMap: { collection, collection_view } } = await loadPageChunk({
chunkNumber: 0,
limit: 50,
verticalColumns: false,
cursor: {stack: [[{table: "block", id: ..., index: 0}]]}
})
...
})

const Page = withViews(({ tweets, views, books }) => (
return
<>
<Post tweets={tweets}>
<P>
Last night I <a href="https://twitter.com/rauchg/status/1289761175253602305" target="_blank">
asked a question</a> on Twitter: what books have you read more than once?
</P>
...
</Post>
<div className="books">
{
books.map(book => {
return <Book key={book.URL} {...book} />
})
}
</div>
</>
)

function Book({ URL, Name, Image, ASIN, Votes }) {
...
return <main ref={targetRef}>
<a href={URL} target="_blank">
<span className="image" style={{
backgroundImage: isInViewport ? `url(${Image ||
`https://images-na.ssl-images-amazon.com/images/P/${ASIN}._LZZZZZZZ_.jpg`
})` : ''
}} />
<span className="title">
{ Name }
{
Votes > 1 ?
<span className="votes">🔥 { Votes }</span> : null
}
</span>
</a>
</main>
}

Let's play with this source.

Testing The Book Component

How does the Book component render when there is no image? Or no votes? Or a single vote? Or lots of votes? Answering all this questions might be hard by just looking at the source code. I know this is not an application meant to be money-making enterprise, but if we cannot easily test a non-production piece of code, would we know how to test the production code? Practice makes perfect, so let's practice.

I have forked the blog repo to github.com/bahmutov/blog-1 and cloned it locally. To write the tests I have installed cypress and cypress-react-unit-test. Recently we have added Next.js support, thus I wanted to export the Book component and start testing.

/pages/2020/books-people-reread.js
1
2
3
export function Book({ URL, Name, Image, ASIN, Votes }) {
...
}

The test needs to mount the component and pass props - I grabbed them from the published blog post page.

cypress/components/pages/2020/books.spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mount } from "cypress-react-unit-test";
import { Book } from "../../../../pages/2020/books-people-reread";

describe("Book", () => {
it("works", () => {
const props = {
URL:
"https://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034",
ASIN: "0671027034",
Name: "How to Win Friends & Influence People",
Votes: 100,
};
mount(<Book {...props} />);
cy.get("a").should("have.attr", "href", props.URL);
});
});

The component renders in the browser controlled by Cypress and I can see it. The test can validate the result using all standard Cypress commands. Cypress is watching the spec file and every file it includes, thus every time I save the changed test the test reruns automatically. I can change the component's source code and the test reruns too. Thus I can play with the look and behavior of the component to verify it does its thing as expected. The video below shows the component rendering different vote tallies while I change the props; then I change the component's style to see how the votes badge looks in different colors.

Tip: since Cypress bundles the spec and the component, there is no server to start to run these component tests, unlike with the full end-to-end tests that need an url to load. Thus we run tests using simply yarn cypress open to open Cypress GUI, or with yarn cypress run to execute all tests headlessly.

Testing The Page Component

If we can mount the Book component and test it "live" like a real mini web application, what about the Page component? Why not? Let's write a component test to see how multiple books are displayed on the page. All we need is feed the exported Page (that's the default export) a list of books and let the test run.

cypress/components/pages/2020/books.spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { mount } from "cypress-react-unit-test";
import Page, { Book } from "../../../../pages/2020/books-people-reread";

describe("Books page", { viewportWidth: 1024, viewportHeight: 1024 }, () => {
// tip: move books list to the fixtures folder and load
// using https://on.cypress.io/fixture or by directly importing it
const books = [
{
URL:
"https://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034",
ASIN: "0671027034",
Name: "How to Win Friends & Influence People",
Votes: 7,
},
...
];
it("renders bunch of books", () => {
const props = {
tweets: 101,
views: 500,
books,
};
mount(<Page {...props} />);
});
});

The test loads and shows the blog post

The Page component test

Note: ignore the error message - I have opened an issue to find a way to fix this.

Do you see the tiny user interface problem with our Book component? The votes badge can split across the lines, and it should not. Here is a video of me fixing the issue using Cypress live reload to see the results.

Before you wave away the badge issue as an artifact of the test runner - no, it is real, you just need to find a browser width that shows it.

The original blog shows the same badge behavior we just fixed

Test Realistically

A while ago someone named Guillermo Rauch (must be a namesake of that Guillermo who loves reading) tweeted the following about testing:

Test using mostly integration tests

I have thought about this message for a while, and even responded in my conference talk at AssertJS, slides. Of course, my response is a lot less popular - I love my friends, who help me keep low profile by not trending my tweets too much 😋.

Write unit and end-to-end tests only

Let me explain the naming "unit" tests I used above.

In my opinion, the component tests in a web application are really exercising units of a web app interface, thus I called my library for mounting the component inside a real browser cypress-react-UNIT-test, same for cypress-vue-unit-test, cypress-svelte-unit-test, etc. Now we are moving away from this nomenclature, since it is confusing the users, now we explicitly call such tests the "component tests". Still, with this in mind you can see the logic of my tweet above.

Today I would like to amend my response. Yes, test the application's behavior using end-to-end tests to verify the code has been built, configured, and deployed correctly. Testing the full preview deploys using Vercel is a good example I have described in the blog post develop-preview-test. By the way, Vercel is run by someone named Guillermo Rauch too - is this a super common name or what?!

The component testing I have shown in this blog post is not that different from end-to-end tests. The component is running as a mini web application in a real browser. You interact with the component using Cypress commands that use user interface without any knowledge of the underlying framework - similar to the way a real user would.

These e2e and component tests are realistic. And that's a very good thing.