One of the biggest advantages of React is undoubtedly using Enzyme (made by Airbnb) to test components, while taking full advantage of the virtual DOM. We can also explore Jest’s powerful tools as a snapshot and create a mock test for external modules. But in the case of Jest, it is not unique to React, though its popularity grew at the same time that React became the framework of the time and it fits well with this paradigm.
But how to test these components in a useful and efficient way?
These tests can also be part of your continuous integration, in the work process and in the validation, if you want to validate it in different environments.
I’ll show you a practical example of a component that was isolated to simulate a test of a form fill with credit card payment option. I will also show how we were able to test this component without having to go through the interface.
In addition, I’ll show you how to integrate it as part of the build, running tests on a remote server when a push to the repository is performed.
Checkout Component
As a good practice, we’ve built a component called Checkout, where we’ll fill out credit card information, user information and shipping.
This component contains states, properties (props) that deal with the different interactions in filling in the form fields by a user.
It is possible to test it in a unitary way and still simulate the events and validation and all actions that happen internally to the component. We can know the states and properties, in addition to being able to configure different states and see what happens in the component and simulate various scenarios of tests.
Here’s the base code for the Checkout component we’re going to test:
import React, { Component } from 'react' import PropTypes from 'prop-types' import { injectStripe } from 'react-stripe-elements' class CheckoutForm extends Component { constructor (props) { super(props) this.handleSubmit = this.handleSubmit.bind(this) this.onChange = this.onChange.bind(this) this.state = { email: null, fullname: null, authenticated: false, userId: null, error: { fullname: false, email: false, payment: false, message: 'loading' }, paymentRequested: false } } handleSubmit (ev) { this.props.stripe .createToken({ name: this.state.fullname }) .then(({ token }) => { // do something with the token }) .catch(e => { // eslint-disable-next-line no-console console.log('error to create token') // eslint-disable-next-line no-console console.log(e) this.props.addNotification('Erro no pagamento') this.setState({ paymentRequested: false }) }) // However, this line of code will do the same thing: // this.props.stripe.createToken({type: 'card', name: 'Jenny Rosen'}); } onChange (ev) { ev.preventDefault() let formData = {} formData[ev.target.name] = ev.target.value this.setState(formData) this.setState({ paymentRequested: false }) } componentDidMount () { const { user } = this.props if (user && user.id) { this.setState({ authenticated: true, fullname: user.name, email: user.email, userId: user.id }) } } render () { const logged = this.state.authenticated const { user } = this.props return ( ) } } CheckoutForm.propTypes = { stripe: PropTypes.object, onPayment: PropTypes.func, task: PropTypes.any, onClose: PropTypes.func, addNotification: PropTypes.func, itemPrice: PropTypes.any, user: PropTypes.object } export const CheckoutFormPure = CheckoutForm export default injectStripe(CheckoutForm)
Here we have a React component that has the states referring to the form fields, which changes according to change events in the fields. This means that we fill in the states when the user types the data, as is often used in React applications.
As this component depends on another component, in this case Stripe, I decided to make a modification that makes it easier to test it, because I do not want to test the full payment stream, just make sure that the component writes the form data to be sent posteriorly.
In this case, I have exported the pure component and connected the component to Redux in another part, so it is exposed as a module available to both.
Environment Required to Run Jest with React
In order for your project to recognize React and be able to process JavaScript written in different variations of JavaScript as ES5, you need to add a Babel configuration:
{ "presets": ["es2015", "react"] }
What Do We Want to Test?
To perform component testing, it would be ideal to do it before any code, but in the real world we often develop without testing and then we have to chase after the injury. This is why when we develop with TDD. We create the design according to the tests and this often affects the whole architecture. When we did not think about the tests, we created dependent components and discovered how much our code was not efficient when we had to test. Therefore, it is important that at least minimal testing is part of development.
In the case here, we have a case of a test that will be added later, and that it was helpful to cover it with test anyway. We cover with a test to get to test a part that we often have bugs.
Adding Tests and Evolving Code
In this example of the tests, we use the Enzyme with the mount component, that creates all structure of a React component and an API that can perform several actions, such as changing a state and applying events, as we did next:
import React from 'react' import { CheckoutFormPure } from '/src/components/checkout/checkout-form' import { mount, configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-15' configure({ adapter: new Adapter() }) describe('components', () => { describe('checkout component', () => { it('should start a new checkout form with empty state', () => { const component = mount() expect(component).toEqual({}) expect(component.state().fullname).toEqual(null) expect(component.state().email).toEqual(null) component.unmount() }) it('should start a new checkout and set state', () => { const component = mount() component.setState({ fullname: 'foo', email: 'mail@example.com' }) expect(component).toEqual({}) expect(component.state().fullname).toEqual('foo') expect(component.state().email).toEqual('mail@example.com') component.unmount() }) it('should start a new checkout and check if a payment is requested and change state', () => { const component = mount() component.find('input').first().simulate('change', { target: { name: 'fullname', value: 'Foo me' } }) component.find('form').simulate('submit') expect(component).toEqual({}) expect(component.state().paymentRequested).toEqual(true) expect(component.state().fullname).toEqual('Foo me') component.unmount() }) it('should set the username and email from a logged user', () => { const component = mount() expect(component).toEqual({}) expect(component.state().fullname).toEqual('Foo me') expect(component.state().email).toEqual('foo@mail.com') component.unmount() }) }) })
We performed some tests in which we changed the internal state of the component and we verified if this change impacts the state of fact, a good way to start testing the basics, because it is always good to follow baby steps.
Then we add an event simulation by changing the fields of the form and seeing if it will change the state as expected. With this we have a practical test and from there carry out several actions and continue with the testing the component, always seeking to have the minimum of dependence possible. As explained in this article about patterns of React components that I spoke about earlier, we have to separate the components in components and connected components.
Conclusion
We have learned here how to create React component tests in practice, controlling the different states and verifying if we have the expected result. This was an example of a project already underway, showing how we can integrate React tests later into a project with Jest in an easy way and perform essential tests without having to modify the whole project or start from scratch. We can also get this process done in the CI (continuous integration) so that the test is performed in any integration of the code, and with that, when others send modifications, the tests ensure that the component still has its basic operation.
About the Author
Diogo Souza works as a Java Developer at PagSeguro and has worked for companies such as Indra Company, Atlantic Institute and Ebix LA. He is also an Android trainer, speaker at events on Java and mobile world.