Testing In React
Last Updated November 10, 2020
Testing is a crucial weapon of any developer's arsenal. Testing ensures your code actually does what you coded it to do. Therefore, understanding how to write good tests is vital.
Having said that, this is not an article on what testing is, nor is it intended to teach you how write tests. The intention of this is teach you how to write tests in React. Now, along with that, you may learn some things about what testing is or how to write tests in general. But, in the course of learning how to test in React you may end up learning some of the fundamentals, as well.
Why Write Tests?
We can all agree testing is important. After all, there are entire divisions of IT departments specifically devoted to testing software. Why, then, should we test? If there are people paid to do it, why should we? Well, first of all, programmatic testing is faster than manual testing. So, increased efficiency is one reason. Secondly, programmatic testing is more deterministic, less error prone. Which, in turn, means increased accuracy. A by-product of increased efficiency and accuracy is peace of mind. And that peace of mind extends from the hands developing the software all the way to end user using the software.
There's also the idea of professionalism at play here. As a software developer, you are a professional. Much like any other professional: doctors, lawyers, architects, scientists, we are the experts. That's worth repeating: When it comes to developing software, we are the experts. That said, there's a degree of responsibility that comes along with being an expert in some field. Writing good, working software is part of that responsibility. Since writing tests increases the likelihood of good, working software, we ought to write tests. Even further, one could say, with some valid degree of authority, that not writing tests is unprofessional. And, in certain fields, such as where good, working software could very well be the determining factor of someone's life, for example, medical software or transportation (plane, train, car, truck, spaceship) software, one could say not writing tests is dangerous and reckless.
For more insights into why developers should be writing tests, see Uncle Bob Martin's talk on Professionalism. It's well-worth the 45 minutes.
Not All Testing Is Created Equal
Programmatic testing falls under a few different categories. Those categories have different names depending who you ask. For our purposes we'll call them unit, functional, and integration tests. All three serve different purposes. Our focus will mainly be on unit and functional tests.
Tools
Just as this is not an article on what testing is or how to write tests, this is also not intended to teach you all about the specific libraries we'll be using. You'll learn stuff about them, sure. But, if you want to understand them any more than a working capacity then you'll need to seek that out yourself.
We'll be using Jest as our test runner. Under the hood it uses a tool called jsdom, which is basically a browser imitator built in Javascript, running on Node.js.
We'll also introduce React Testing Library as a way to help us write better tests without relying on implementation details. More on that later.
Example
Before we start writing code, test or not, we need to understand what we're going to implement. Here are the requirements:
In this app you are greeted when you enter. Now, new requirements have come in to say goodbye when you leave.See the acceptance criteria below:- Use the containment pattern to create a `PrettyBorder` component.- Use the specialization pattern to create one, general, message component.- That message component should be *composed* or *implemented* for each type of message - in our case, greeting and goodbye.- The goodbye message should display when the Leave button is clicked.- Only one message is displayed at a time.- No message is displayed on startup (this makes it a little tricky)
Here's a sample of the functionality:
With these new requirements we can now write some tests. Let's take them line by line:
Use the containment pattern to create a PrettyBorder
component.
This is an implementation detail. It has no bearing on the app's functionality. We do not need to test this.
Use the specialization pattern to create one, general, message component.
Again, this is an implmentation detail, no need to test it.
That message component should be composed or implemented for each type of message - in our case, greeting and goodbye.
This simply refers to the requirement from above. No test.
The goodbye message should display when the Leave button is clicked.
Finally, our first "action". This is something that is testable. Do you see the difference in this requirement over the others? It's a new expectation.
Expectations Over Implementations
You'll hear that a lot when dealing with frontend testing. Expectations Over Implementations. It's a phrase used to indicate we write tests according to expectations, not implementations.
What this means is I can rewrite my code as much as I want and as long as my expectations stay the same I never have to modify my tests. This is in stark contrast to how you may have written tests in the past. For example, in the .NET world you often write unit tests against specific methods. And there's nothing wrong with that. However, in the world of frontend development we have the priviledge of a virtual DOM. That means we have the ability to actually pretend to be the user programatically. That's an opportunity we cannot pass up. When it comes to testing, you want your tests as close to the user as possible. And let's face it, you can't get much closer than programatically clicking button and expecting behaviors can you?
That's why I choose React Testing Library to write tests. It was written with expectation testing in mind.
To give a real, concrete example of what I'm talking about let's look at the code for the component we'll be testing.
/////////////////////////////////// Some code excluded for brevity. See bottom of the article for full code./////////////////////////////////class Sample extends React.Component {constructor(props) {super(props)this.state = { status: this.STATUS.neverEntered }}STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }handleEnter = () => this.setState({ status: this.STATUS.entered })handleLeave = () => this.setState({ status: this.STATUS.left })handleReset = () => this.setState({ status: this.STATUS.neverEntered })render() {let messagelet button = <Outside handleClick={this.handleEnter} />const status = this.state.statusif (status === this.STATUS.entered) {message = <GreetingMessage />button = <Inside handleClick={this.handleLeave} />} else if (status === this.STATUS.left) {message = <GoodbyeMessage />button = <Outside handleClick={this.handleEnter} />}return (<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>{button}{ ' ' }<Reset handleClick={handleReset} />{message}</div>)}}
Now, let's say we want to refactor this to be a function component.
/////////////////////////////////// Some code excluded for brevity. See bottom of the article for full code./////////////////////////////////function Sample() {const STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }const [ status, setStatus ] = useState(STATUS.neverEntered)const handleEnter = () => setStatus(STATUS.entered)const handleLeave = () => setStatus(STATUS.left)const handleReset = () => setStatus(STATUS.neverEntered)let messagelet button = <Outside handleClick={handleEnter} />if (status === STATUS.entered) {message = <GreetingMessage />button = <Inside handleClick={handleLeave} />} else if (status === STATUS.left) {message = <GoodbyeMessage />button = <Outside handleClick={handleEnter} />}return (<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>{button}{ ' ' }<Reset handleClick={handleReset} />{message}</div>)}
Even though those components have two completely different implementations their expectations are the same. Because of that the tests we are about to write will work seamlessly for either one.
The First Test
Let's look at the first test.
it('should show Goodbye message after clicking Leave', () => {const { getByText, queryByText } = render(<Solution />) // this simply renders the components and pulls out some useful methods from the @testing-library/react libraryconst enterButton = getByText(/Enter/i) // first, we get the Enter buttonenterButton.click() // then we click the Enter buttonconst leaveButton = getByText(/Leave/i) // once the Enter button is clicked, the Leave button should be availableleaveButton.click() // then we click it, hopefully displaying our new goodbye messageconst goodbye = queryByText(/See you later!/i) // next, we'll get that goodbye message and verify it exists (the purpose of the test)expect(goodbye).toBeInTheDocument() // all we want to do it make sure it exists on the page})
The name of the test says it all. it('should show Goodbye message after clicking Leave'
. Walking through the code (see the comments),
you can see what we're doing is just programmatically doing what it would take to manually test this requirement.
If this hasn't been coded yet the test should fail.
By the way, you can run your tests with yarn test
from your terminal in the root directory
(the same directory that your package.json
file exists).
I recommend writing your test first, before writing any code. Why? Because it allows you to "spec" out what you expect your component to do. This is called Test Driven Development, or TDD. It's a discipline more than anything, but if you subscribe to it, you'll find the upfront time cost ends up saving time in the near and distant future.
If you haven't yet, go ahead and write the code to attempt to make this test pass. We never even have to load the browser! - you should always take a look, though. It's possible to write the wrong test!
**I'll put the full working code example at the end of this article.
Only one message is displayed at a time.
Here we have another expectation. We need to make sure that for each button click, Enter
or Leave
, that the proper message is displayed.
it('should show Greeting message after clicking Enter', () => {const { getByText } = render(<Start />)const enterButton = getByText(/Enter/i)enterButton.click()const greeting = getByText(/Hello there!/i)expect(greeting).toBeInTheDocument()})it('should hide Greeting message after clicking Leave', () => {const { getByText, queryByText } = render(<Start />)const enterButton = getByText(/Enter/i)enterButton.click()const leaveButton = getByText(/Leave/i)leaveButton.click()const greeting = queryByText(/Hello there!/i)expect(greeting).toBeNull() // note we are making sure the greeting does NOT exist here})
The two tests above ensure that when Enter
is clicked the Greeting message is displayed and that when the Leave
button is clicked the message goes away.
Let's look at how we would test that the Goodbye message goes away when we click Leave
.
it('should hide Goodbye message after clicking Enter/Leave/Enter', () => {const { getByText, queryByText } = render(<Solution />)let enterButton = getByText(/Enter/i)enterButton.click()const leaveButton = getByText(/Leave/i)leaveButton.click()let goodbye = queryByText(/See you later!/i)expect(goodbye).toBeInTheDocument()enterButton = getByText(/Enter/i)enterButton.click()goodbye = queryByText(/See you later!/i)expect(goodbye).toBeNull()})
This test runs a little longer. You get an indication that it will take more steps performed to test this behavior just by the title
it('should hide Goodbye message after clicking Enter/Leave/Enter'
. The title clearly lays out we'll need three button clicks to
achieve our desired state.
Notice the first expect()
, expect(goodbye).toBeInTheDocument()
. That's not really part of the test. However, I chose to put it there because
it verifies an expectation that we need in order to get to the state we want to be in. Let's imagine I left that line out. Now, the test is that
the Goodbye message does not exist when we're finished with our actions. If that first expect()
did not exist and the test passed, then we
never know if clicking Leave
actually hid the message. The message could've never existed in the first place! Now, I understand this is actually
tested in the prior requirement. So, having that line there isn't necessary, nor is it even considered a best practice, from what I know. It's
simply something I do to give me more peace of mind when writing tests.
A potential gotcha
Here's something important to note, that might otherwise get overlooked. Notice that enterButton
is declared with let
. That's because we need
to go get it again, it needs a second assignment. That's because that instance of the enterButton
actually went away after it was clicked and
the Leave
button took its place. So, after clicking Leave
with leaveButton.click()
and before clicking Enter
again with enterButton.click()
we need to go get Enter
button again. This is an important point to remember, as it could cause some headache trying to debug a busted test.
No message is displayed on startup (this makes it a little tricky)
Here we also have another behavior or expectation. However, it also existed in the Start
component so we already have a test for it.
it('should not show Greeting message on render', () => {const { queryByText } = render(<Start />)const greeting = queryByText(/Hello there!/i)expect(greeting).toBeNull()})
The (this makes it a little tricky) note is really only for the development portion. However, having the test already there can give us confidence in what we're writing when implementing it.
That's it! We're done! I hope this example serves to help you get started in writing good frontend tests. If you have any questions or suggestions about the article or anything else, for that matter, let me know! I'm happy to help in any way I can!
Now, here's the full component code I promised.
function PrettyBorder(props) {return (<div style={{ border: '5px solid purple', margin: 20, padding: 20, fontSize: 16 }}>{props.children}</div>)}const Message = (props) => <PrettyBorder>{props.content}</PrettyBorder>const GreetingMessage = () => <Message content='Hello there!' />const GoodbyeMessage = () => <Message content='See you later!' />const Inside = (props) => <button onClick={props.handleClick}>Leave</button>const Outside = (props) => <button onClick={props.handleClick}>Enter</button>const Reset = (props) => <button onClick={props.handleClick}>Reset</button>function Sample() {const STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }const [ status, setStatus ] = useState(STATUS.neverEntered)const handleEnter = () => setStatus(STATUS.entered)const handleLeave = () => setStatus(STATUS.left)const handleReset = () => setStatus(STATUS.neverEntered)let messagelet button = <Outside handleClick={handleEnter} />if (status === STATUS.entered) {message = <GreetingMessage />button = <Inside handleClick={handleLeave} />} else if (status === STATUS.left) {message = <GoodbyeMessage />button = <Outside handleClick={handleEnter} />}return (<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>{button}{ ' ' }<Reset handleClick={handleReset} />{message}</div>)}