React Lesson 2 (Part 3): Unit Testing

Let’s add some unit testing to our little text editing widget that we began in React Lesson 2. At this point I’m going to create a new branch from my existing setstate-horror branch. While currently on the setstate-horror branch, I type git checkout -b tiny-text-commit. This creates a new branch copied from the old one and makes that my current branch. We are still in the same repo, which is https://github.com/xerocross/react-examples.

In the package.json file, we have a “test” script defined simply “react-scripts test”. There are no configuration files yet because I still haven’t run eject. So I actually have no idea what testing framework we are using here. We used create-react-app to build the initial package. It came with a file App.test.js that looks like Jasmine syntax. Also I see this:

const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);

So somehow in the tests we have access to a document object. But when I execute yarn test in the shell, it does not launch a browser window. The test results appear in the console. So what’s going on? Headless browser?

Reading here ( https://facebook.github.io/create-react-app/docs/running-tests ) it says they use Jest as the test runner. I love Jest. But this is not the Jest language. Jest doesn’t use a weird “it” function to try to make the function calls look more like English sentences. But Jest does offer ways to mock a browser without actually using a real browser. Reading further, it looks like React itself added this “it” function. I will not be using it. I like Jest already, so I’m going to write in Jest.

Most of the functioning of this widget involves flow of state using React’s setState function. We really can’t do any sort of testing at all without somehow using React inside our tests. It looks like these libraries will be helpful: react-testing-library and jest-dom. So I’m installing both of them as dev dependencies. Let’s figure out how to use react-testing-library.

I created a test file and added a “commit text and reset to last commit functions as expected” test using react-testing-library. I have not written tests in this style before. This library is very opinionated about testing through DOM interactions like a user interaction. For example, we don’t grab internal functions and unit test them in isolation. Instead, we render the component and write a script that mimics a sequence of user behaviors (input value changes, click events, etc), and then we check that this has resulted in the expected state. I wrote a test that seems to work. Also, importantly, it fails if I change the expected behavior to something wrong. (That’s just a quick reality test to see if I’m using the tools correctly.) But right now I think I’m probably missing something important. My test function looks very synchronous, but I thought React did state updates asynchronously.

OK, I see the answer to that now. The package react-dom/test-utils offers this function act that you wrap around events and such, around things that fire some asynchronous behavior. For example:

act(() => {
    button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});

The act function takes care of the asynchronous nature of this. And react-testing-library automatically wraps event fires and such in this act function. So here is my first test that I wrote using these tools, and as far as I can tell this is correctly done.

test("commit/reset works", ()=> {
    const {  getByText, container } = render(<MyStatefulComponent />)
    let textarea = container.querySelector("textarea");
    fireEvent.change(textarea, { target: { value: 'adam' } });
    let commitButton = getByText("commit");
    fireEvent.click(commitButton);
    fireEvent.change(textarea, { target: { value: 'adam cross' } });
    let resetButton = getByText("reset to last commit");
    fireEvent.click(resetButton);
    expect(textarea.value).toBe("adam");
});

This test grabs the textarea and simulates the user typing in “adam”. Then it gets the “commit” button and fires a click event on it. We are testing the handlers that get executed when these events fire, but we don’t touch those methods directly. After “commit”, we fire a text change so that it reads “adam cross”. Then we fire a “reset” button click. The expected result of that is that the text goes back to the most recent commit, and that is when it said “adam”. My test runner says that is what happened. Success.

This react-testing-library is a wrapper around dom-testing-library ( https://testing-library.com/). These guys are the ones advocating this particular style of testing. And the React team seems to encourage using this. I kinda like it actually. Once we have the basic binding functions offered specifically by react-testing-library, then what we need to learn is dom-testing-library because that is what is actually doing the work.

By the way: this style of testing reminds me forcibly of what our QA people at LendingTree did using Selenium. However, the fact that we are not using a real browser for these tests is an important difference. In the long run, I’m not sure I see the appeal of writing tests in this style without executing them in actual browsers.

I’m writing more tests now. You can see them all in the source code, but they are all quite similar. Also, I’m happy to say that my battery of tests all succeeded, which leads me to believe we made a functional little widget.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s