Test your React Components with Nightwatch and Testing Library

A look at the popular Testing Library with Nightwatch — and more

Andrei Rusu
Pineview Labs
7 min readJan 10, 2023

--

Nightwatch + Testing Library

We will build a detailed example of a React project with Vite and then use Nightwatch and Testing Library to test the components. We’ll use the Complex example available on the React Testing Library docs, written with Jest.

In this tutorial, we’ll cover the following:

  1. Set up a new React project with Vite, which is also what Nightwatch uses internally for component testing
  2. Install and configure Nightwatch and Testing Library
  3. Mock API requests with the @nightwatch/api-testing plugin
  4. Write a complex React component test using Nightwatch and Testing Library

Step 0. Create a New Project

To get started, we’ll create a new project with Vite:

npm init vite@latest

Select React and JavaScript when prompted. This will create a new project with React and JavaScript.

Step 1. Install Nightwatch and Testing Library

Testing Library for React can be installed with the @testing-library/react package:

npm i @testing-library/react --save-dev

To install Nightwatch, run the init command:

npm init nightwatch@latest

Select Component testing and React when prompted. This will install nightwatch and the @nightwatch/react plugin. Choose a browser to install the driver for. We'll be using Chrome in this example.

1.1. Install @nightwatch/testing-library plugin

Since v2.6, Nightwatch has provided its plugin for using the Testing Library queries directly as commands. We’re going to need it to write our test later on, so let’s install it now:

npm i @nightwatch/testing-library --save-dev

1.2 Install @nightwatch/apitesting plugin

The example contains a mock server that is needed to test the component. We’ll be using the integrated mock server that comes with the @nightwatch/apitesting plugin. Install it with the following:

npm i @nightwatch/apitesting --save-dev

Step 2. Create the Login Component

We’ll use the same component as in the React Testing Library docs. Create a new file src/Login.jsx and add the following code:

// login.jsx
import * as React from 'react'

function Login() {
const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
resolved: false,
loading: false,
error: null,
})

function handleSubmit(event) {
event.preventDefault()
const {usernameInput, passwordInput} = event.target.elements

setState({loading: true, resolved: false, error: null})

window
.fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value,
}),
})
.then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
.then(
user => {
setState({loading: false, resolved: true, error: null})
window.localStorage.setItem('token', user.token)
},
error => {
setState({loading: false, resolved: false, error: error.message})
},
)
}

return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameInput">Username</label>
<input id="usernameInput" />
</div>
<div>
<label htmlFor="passwordInput">Password</label>
<input id="passwordInput" type="password" />
</div>
<button type="submit">Submit{state.loading ? '...' : null}</button>
</form>
{state.error ? <div role="alert">{state.error}</div> : null}
{state.resolved ? (
<div role="alert">Congrats! You're signed in!</div>
) : null}
</div>
)
}

export default Login

Step 3. Create the Component Test

One of the founding principles of Testing Library is that tests should resemble how users interact with the application as much as possible. When writing component tests in Nightwatch using JSX, we need to write the test as a component story using the Component Story Format, a declarative format introduced by Storybook.

This enables us to write tests focusing on how the component is used rather than how it’s implemented, which aligns with the Testing Library philosophy. You can read more about this in the Nightwatch docs.

The great thing about using this format to write our tests is that we can use the same code to write stories for our components, which can be used to document and showcase them in Storybook.

3.1 Login With Valid Credentials Test

Create a new file src/Login.spec.jsx and add the following code, which does the same as the complex example written with Jest:

To render the component using JSX in Nightwatch, we create an export for the rendered component, optionally with a set of props. The play and test functions are used to interact with the component and verify the results.

  • play is used to interact with the component. It's executed in the browser context, so we can use the screen object from Testing Library to query the DOM and fire events;
  • test is used to verify the results. It's executed in the Node.js context, so we can use the Nightwatch browser object to query the DOM and verify the results.
// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/login'

export default {
title: 'Login',
component: Login
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
};

LoginWithValidCredentials.test = async (browser) => {
// verify the results
};

Add the mock server

The example uses a mock server to simulate a login request. We’ll be using the integrated mock server that comes with the @nightwatch/apitesting plugin.

For this, we’ll use the setup and teardown hooks which we can write directly in the test file. Both hooks are executed in the Node.js context.

We also need to set the login endpoint to http://localhost:3000/api/login in the Login component, which is the URL to the mock server.

Complete test file

The complete test file will look like this:

// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/Login'

let server;
const token = 'fake_user_token';
let serverResponse = {
status: 200,
body: {token}
};

export default {
title: 'Login',
component: Login,
setup: async ({mockserver}) => {
server = await mockserver.create();
server.setup((app) => {
app.post('/api/login', function (req, res) {
res.status(serverResponse.status).json(serverResponse.body);
});
});

await server.start(mockServerPort);
},

teardown: async (browser) => {
await browser.execute(function() {
window.localStorage.removeItem('token')
});

await server.close();
}
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: {value: 'chuck'},
});

fireEvent.change(screen.getByLabelText(/password/i), {
target: {value: 'norris'},
});

fireEvent.click(screen.getByText(/submit/i))
};

LoginWithValidCredentials.test = async (browser) => {
const alert = await browser.getByRole('alert')
await expect(alert).text.to.match(/congrats/i)

const localStorage = await browser.execute(function() {
return window.localStorage.getItem('token');
});

await expect(localStorage).to.equal(fakeUserResponse.token)
};

Debugging

One of the main benefits of using Nightwatch for component testing, besides having the same API available for end-to-end testing, is that we can run the tests in a real browser, instead of a virtual DOM environment, such as JSDOM.

This allows us to use the Chrome Dev Tools to debug the tests.

For example, let’s go ahead and add a debugger statement in the LoginWithValidCredentials.play function:

LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: {value: 'chuck'},
});

fireEvent.change(screen.getByLabelText(/password/i), {
target: {value: 'norris'},
});

debugger;

fireEvent.click(screen.getByText(/submit/i))
};

Now, let’s run the test with --debug and --devtools flags:

npx nightwatch test/login.spec.jsx --debug --devtools

This will open a new Chrome window with the Dev Tools open. We can now set a breakpoint in the Dev Tools and step through the code.

Debugging with Chrome DevTools

3.2 Login With Server Exception Test

The original example from the Testing Library docs also includes a test for the case when the server throws an exception.

Let’s try to write the same in Nightwatch. This time we’ll use just the test function since we can also interact with the component. As we've mentioned earlier, the test function is executed in the Node.js context and receives the Nightwatch browser object as an argument.

We’ll also need to update the mock server response to return a 500 status code and an error message. This we can easily accomplish by writing a preRender test hook on the LoginWithServerException component story.

export const LoginWithServerException = () => <Login />;
LoginWithServerException.preRender = async (browser) => {
serverResponse = {
status: 500,
body: {message: 'Internal server error'}
};
};

LoginWithServerException.test = async (browser) => {
const username = await browser.getByLabelText(/username/i);
await username.sendKeys('chuck');

const password = await browser.getByLabelText(/password/i);
await password.sendKeys('norris');

const submit = await browser.getByText(/submit/i);
await submit.click();

const alert = await browser.getByRole('alert');
await expect(alert).text.to.match(/internal server error/i);

const localStorage = await browser.execute(function() {
return window.localStorage.getItem('token');
});

await expect(localStorage).to.equal(token)
};

4. Run the Test

Finally, let’s run the test. This will run the LoginWithValidCredentials and LoginWithServerException component stories in Chrome.

npx nightwatch test/login.spec.jsx

To run the test without opening the browser, we can pass the --headless flag. If all goes well, you should see the following output:

[Login] Test Suite
────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (1134ms).
Using: chrome (108.0.5359.124) on MAC OS X.

Mock server listening on port 3000

Running <LoginWithValidCredentials> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
✔ Expected element <LoginWithValidCredentials> to be visible (15ms)
✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/congrats/i" (14ms)
✔ Expected 'fake_user_token' to equal('fake_user_token'):

✨ PASSED. 3 assertions. (1.495s)

Running <LoginWithServerException> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
✔ Expected element <LoginWithServerException> to be visible (8ms)
✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/internal server error/i" (8ms)
✔ Expected 'fake_user_token' to equal('fake_user_token'):

✨ PASSED. 3 assertions. (1.267s)

✨ PASSED. 6 total assertions (4.673s)

Conclusion

That’s it! You can find the complete code for this example in the GitHub repository. PRs are welcome.

Feel free to drop by the Nightwatch Discord if you have any questions or feedback.

--

--