Adding tests to a login form using yup and React Hook Form
Client-side testing is a massive topic. If you’ve read “Developing a Client-Side Testing Strategy,” you’ll have been introduced to the best practices and challenges faced when selecting a client-side testing strategy.
We could choose to take a deeper look at any of the following below:
- Testing devices and different browsers where users view your product
- Static types and linters to validate values and syntax during development
- Testing CSS, comparing the final design, and markup results
- Testing a native application and the testing suites available
- Manual testing practices
- Using visual libraries, e.g., Storybook
But for now, let's keep it simple and do some unit, integration, and end-to-end tests.
Low and behold, the user Login — every app has one.
Okay, so what are some of the things we want to test for here?
- Check that the Login form renders correctly.
- Check that input validation works on form submit.
- Check that the form submits correctly when inputs have content.
- Check that the request works correctly on success.
- Check that request works correctly on error.
- Check that a token is saved to local storage after a successful request.
The Tools
Jest —Jest is a JS framework created by Facebook that is included in React and acts as a test runner, assertion library, and mocking library. It can also be used for snapshot testing.
React Testing Library — This builds on DOM Testing Library by adding APIs for working with React components and utilities to query the DOM.
Cypress— Cypress is a JavaScript-based end-to-end testing framework. It‘s built on top of Mocha, which allows us to run automated tests in our browser, button-bashing everything to ensure we get the expected results.
testing-library/cypress — Let’s use the same utility methods in our unit, integration, and end-to-end tests.
Jest-dom — This testing library allows Jest to write tests that assert various things about the state of the DOM, making writing tests faster and more enjoyable.
yup —This is our schema validation library.
We’ll start by creating a user Login component. It’ll wrap our Form and contain our Request, Header, Alert, and some UI states.
Next, let’s create a Form component to be imported by our Login.
We’ll import the useForm
Hook and yup validation, along with Textfield
for inputs and Button
from MaterialUI
.
React Testing Library
< Nice! />
I'll assume we‘re familiar with everything up to this point. Now, let’s wrap our Login in some tests.
The first thing we can check is that the page renders correctly, or the smokescreen test, as I’ve heard it called. So how do we do that?
Well, we need to understand a little bit about React Testing Library first.
Essentially, React Testing Library renders our components, replicating the DOM that the end-user will see. It then provides utility methods to interact with this DOM. This is different from another popular testing library, Enzyme. Enzyme works directly with React components, giving you access to the internal workings of your components, such as updating the state.
Both philosophies could work for us. However, my preference is React Testing Library, since it tests and communicates with our application the same way a user would. So, rather than set the state of a component, you reproduce the actions a user would do to reach that state.
I cover the importance of this further in my article on developing a client-side testing strategy, mentioned above.
React Testing Library & Jest — Unit & Integration Tests
To render a component, we’ll import render from React Testing Library.
After rendering, we can use React Testing Library’s utility methods to search for the presence of a keyword in the DOM.
<Pretty straight-forward, right?!/>
Next, we’ll move our attention to the Form. We want to check that validation works on the Form submit event and that validation errors are present.
The useform
Hook we rely on makes this a little trickier since it triggers events in the DOM asynchronously. This is not a problem for React Testing Library.
We again use utility methods to check for the presence of validation errors in the rendered DOM and populate the form to make sure it submits correctly.
This is nice since it allows us to isolate our Form component, without having to do a mock API request, and could be helpful for when I decide to reuse the Form in other areas of the application.
Next up, I want to check that I can submit my Form, that the request works, and that a token is added to my localStorage
.
We can mock data that is returned from the request. In this case, we mock the user_token.
I want to check that the Login behaves as I would expect after a successful request. Again, I populate my Form, submit it, and check that my alert response is as expected and that a token is saved to my localStorage
.
So with that, we can see that we’re able to achieve some pretty decent test coverage via React Testing Library and Jest with a combination of unit tests and integration tests. But we only scratched the surface here, testing DOM Element, Events, an HTTP request, and Asynchronous Actions.
React Testing Library will also allow you to test React Router, Redux, Context, and SnapShot tests.
What Is Cypress?
Cypress specializes in doing one thing really well. That’s creating a great experience while you write end-to-end tests for your web applications.
It does this by allowing you to write with JS and comes bundled with all the tools you’ll need or are familiar with.
It mimics the user’s behavior on a browser and has similar methods that allow you to search the DOM.
And its browser testing tool is pretty sweet!
Testing libraries Cypress package is a great addition to consider importing.
This allows us to use the same methods for querying the DOM and React Testing Library. I’m a big fan of this arrangement since it provides valuable consistency across our testing strategy.
Cypress End-to-End Testing
Cypress will replicate a user’s behavior updating inputs, firing events, navigating links, and making requests.
We can traverse the DOM, checking for the presence of elements. We can test that the form has valid values and that it submits, as well as the request and response. In fact, if we wanted, we could get Cypress to check the localStorage
for a token and perform all the same tests that we did with React Testing Library.
Thoughts and Conclusion
The name of the game is to get it so that you’re confident that the code you push will be of sufficient quality for user and business requirements. Having this as your North Star will guide you, as you come up with your own unique blend of unit, integration, and end-to-end tests.
What I’ve done for my Login is overkill in many situations. There’s a lot of cross-over between tests, and I may just choose just to add end-to-end to it via Cypress and nothing else.
But let's imagine some other situations. Let’s say that I wanted to re-use that Form component I’ve created, and extend it so I can share it across the application, wherever a Form component is needed. Well, then I’d love to see it isolated with some unit and integration tests wrapped around it, working with mocked data. It would give me confidence that such a key part of the app is well-covered.
The same can be said for requests and custom Hooks. Our Login only uses two utility Hooks that we import, useState
and useForm
. However, if we wanted to create and reuse our own custom Hooks throughout the platform, well then, wrapping unit tests around them will make me feel tingly inside.
The name of the game is confidence.
I hope some of this article helps you next time when you have sweaty palms pushing to production.
Leave a comment