Our first component is going to be the screen that displays the card. We will call this component "Answering." We will call it "Answering" because answering questions is what this component lets the user do. This post will show you how to take the user story about this component and turn it into tests. We will then write Answering using Red, Green, Refactor. In the next post we will change the main App component to show Answering on the screen.
User Story
- The user sees a question displayed on the screen. The user writes an answer to the question. When the user is done with their answer, they click the submit button. The app shows them the answer key. The user compares their answer to the answer key. The user decides they got the question right, and clicks the 'right answer' button. Then the user sees the next question.
We are going to make this user story happen!
Features
Here are the features that we need to have to make the user story possible.
- show the user the question from the card
- a box that the user can type their answer in
- a button to submit the user's answer
- show the user the answer from the card
- a button to record a right answer
- a button to record a wrong answer
Choose Components
This is the reasoning for which components we are going to use.
We'll put all our components in a Container to keep them organized on the screen.
We need to show the user the question. Let's use a Header to show the question.
We could use an Input to give the user a place to type their answer, but Inputs are usually for a single line of text. Flashcard answers can get pretty long, so we'll use a TextArea. TextAreas are a big box that the user can type in.
We will also use a Form. A Form
lets you group inputs together. We need to put the TextArea
inside a Form
because the Semantic UI React TextArea
gets some css styling from being inside a Form
. It won't look right without the Form
.
The buttons are an easy choice. We'll use a Button for the buttons.
We want to show the answer to the user when they are ready to see it. That means sometimes the answer will be hidden, but sometimes it will be shown. We'll use a Transition component to animate the answer when it appears. We'll put a div with a header inside the Transition. But we won't make the Answer component right now. We'll make the Answer component later, in post 3. To keep this post shorter, we won't write tests for the Answer yet. We'll do that in post 3.
Get Ready to Write the Tests
We are making this app using Test Driven Development. That means the first thing we write when we make a component is the tests. The main reason to write tests first is that it helps to break down the problem you are trying to solve. Other good reasons to write tests first are that you will know when your code works. You will also know when changes make something stop working.
Red
We will write the tests in Typescript. We will write the tests using functions provided by Jest and React Testing Library. We will use Jest to run the tests. We'll write a test and see that it fails. That's good! Each test you write should fail because you haven't written the code to pass it yet. This is the "Red" part of red, green, refactor. It is the red part because a failing test will show up red in the test running program.
Green
Then we'll write the code to pass the test and run the tests again. You want to write the least amount of code that you need to pass the test. Once you have done that, the test will pass. This is the "Green" part of red, green, refactor. It is the green part because a passing test will show up green in the test running program.
Refactor
Once the component passes all the tests we are ready to refactor. Refactoring is when you can improve the code. You can make it more efficient. You can make the code easier to read. You can take out parts of the code that are repeated. Then we are ready to write another component or add more features.
Decide What to Test
In this first set of tests we are going to write a test for everything. Once you get more experienced at testing you won't write a test for everything. You will decide what to test and what not to test.
Deciding what to test is important. You want to test the things that matter. You do not want to test the things that don't matter. What matters and what doesn't is something you will get a feel for as you do more testing.
Think about the features we are implementing. Think about the components we decided to use. We will test for the things that matter. We will test for:
- the
Container
that holds everything else - the
Header
that shows the question - the
Button
to skip to the next question - the
TextArea
to write the answer in - the
Button
to submit the answer
Snapshot
We will also put in a snapshot test. The first time you run a snapshot test it records how the component looks. Every time you run the snapshot test after the first time it compares how the component looks now to how the component looked the first time it ran. If it looks different then it fails. Snapshots are useful because they tell you when you changed what shows up on the screen. But you won't want to use snapshot tests in every situation.
Not Showing the Answer Yet
To make this first set of tests easier we aren't going to test for the component that shows the answer yet. We'll do that in Post 7, where we make the separate component that shows the answer.
Writing the Tests
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-1.tsx
The folders have been created so you have somewhere to put the files, but I left the files out so that you can write them yourself. Whenever the tutorial has you make a new test file or component, you'll have to create the file yourself and then write the contents.
Create a new test file named index.test.tsx
in the folder src/scenes/Answering/
. In the test file, write a comment line for each test you are going to write.
//we need
//a container,
//test to see if the question prompt is in the document
//test to see if the Skip button is in the document
//a textarea to write the answer in
//test to see if the Submit button is in the document
//and the snapshot
Add the imports at the top of the file, above your comments.
//React lets us create and display components to the user
//We need to import it so that we can look at the components to test them
import React from 'react';
//testing library gives us methods to test components
//we use render to look at React components
//we use cleanup to clear out memory after tests
import { render, cleanup } from '@testing-library/react';
//extend-expect gives us methods that let us say what we think a component will look like when we test it
import '@testing-library/jest-dom/extend-expect';
//This is the Answering component that we are going to write
//we have to import it so that we can look at it to test it
import Answering from './index';
Then call afterEach(cleanup)
afterEach(cleanup);
afterEach() is a method from Jest. Just like the name says, afterEach is called after each test.
cleanup is a method from React Testing Library. It clears out memory after a test. Each test is going to render some components. We don't want those components to stay around afterwards because they will take up memory. By passing cleanup
to afterEach
we make sure that everything gets cleaned up after each test.
Test for Each Feature
The way that you test components using React Testing Library is you use the render()
method to render the component. Then you search in the rendered result for the feature that you want to test. Then you make your "assertions" about the feature.
We listed 5 features that we want to see in the Answering component:
- Container
- Header to show the question
- Button to skip the card
- TextArea to write the answer in
- Submit button
Answering Test 1: Has a Container
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-2.tsx
I am going to explain the first test line by line. Later in the tutorial, when you are more familiar with the concepts, we won't go over them in so much detail.
it('has a Container', () => {
const { getByTestId } = render(<Answering/>);
const container = getByTestId('container');
expect(container).toBeInTheDocument();
});
What this test does is check to see if there is a particular object in the result of the render.
The First Line of Test 1
it('has a Container', () => {
it() is a method provided by Jest. The it()
method takes two parameters.
The first parameter is the name of the test. The name is a string. The name of this test is 'has a Container.' The name is what Jest will print on the screen when the test runs, succeeds, or fails.
The second parameter is a function. The function is what will be run when the test is run. The last characters in the first line, () => {
are the start of an anonymous function. Look at the last line. See the closing bracket }
on the last line? That's the end of the anonymous function. The lines of code in between the {}
are what will be run every time this test is run.
Using an anonymous function lets you declare the function inside the call to the it()
method instead of having to declare the function somewhere else before passing the function to it()
.
The Second Line of Test 1
const { getByTestId } = render(<Answering/>);
< Answering /> may look like HTML, but it is actually using JSX to call to the element returned by the React component named Answering
. Don't get too confused- we haven't written the Answering
component yet. But once we do, calling it will return some code that will eventually become HTML that can be displayed on the screen. That's what React does!
render() is a function that we get from React Testing Library. You pass a React component to render
, and render
will do basically the same thing that a web browser does. It will turn the code into HTML. But instead of turning the code into words and pictures on the screen, the render
function returns an object. The object that render
returns has a lot of functions in it. Most of the functions are designed to let you search through the code that your component turned into. When you search through the code that your component turned into you are testing if the code looks like you think it should.
const is one of the Javascript commands to declare a variable.
One of the methods that render() returns is called getByTestId
. I'll explain what it does a little later. We want to give our newly declared const variable the value of render(< Answering />).getByTestId
.
When we put the new name of the variable inside the curly brackets { getByTestId } we are telling the compiler three things.
- there is an object on the other side of the equals sign.
- that object has a property called getByTestId.
- we are declaring a variable and giving it the value of the getByTestId property.
We could instead do that like this:
const getByTestId = render(<Answering/>).getByTestId
Doing this with the curly brackets is called Object Destructuring.
const { getByTestId } = render(<Answering/>)
Using Object Destructuring is shorter and easier to read than object.property.
The Third Line of Test 1
const container = getByTestId('container');
getByTestId
In this test we are using the getByTestId method. getByTestId looks through the result of render for the testId that we pass to it. This test is looking for the testId 'container'. If getByTestId finds something with a testId 'container' then getByTestId returns a reference to that object. If getByTestId doesn't find something then it will throw an error.
getByTestId is one query you can use to find things. There are a lot of other queries that you can use to find things. We will use those other queries later in the tutorial.
What are testIds?
Components don't come with testIds. Not all components have testIds. The testId doesn't do anything in the code. It just gives you a way to find a component when you are testing. You can assign a testId to a component when you write the code for the component. Assigning a testId to a component looks like this:
<Answering data-testid='this is the testId I want to give it' />
When we write the Answering component, we will give the Container component a testId 'container'. That way this test will find the Container when we use getByTestId('container').
Looking for the element with a testId 'container' doesn't tell you that the element is a container. It doesn't tell you what's in it. It just tells you that there is (or isn't) an element with a testId 'container.'
The Fourth Line of Test 1
expect(container).toBeInTheDocument();
expect
Expect is a method provided by the Jest library. We use expect()
to look at the element that we pass to it. We use expect to tell us if our assertions about the element are true or not.
Assertions and Matchers
Matchers are how you find objects in the test. Assertions are how you say what you think the result of a test should be. Using Assertions and Matchers together is how you say "I think this element will have this certain value" about the components you are testing.
toBeInTheDocument
One of the methods that expect()
gives us is toBeInTheDocument(). The toBeInTheDocument()
matcher will pass if the object we gave to expect()
is found in the document that we got back from render()
. If the object isn't in the document, then the test will fail.
Why use toBeInTheDocument when there are other ways to to test if something exists?
There are a lot of ways to test if an element exists. Here's an example of a message board post asking about what the best method is. They all tell you something specific and different from other methods. The toBeInTheDocument()
matcher tells you that the element you are looking for exists and is in the document when you looked for it. Other methods might just tell you that the element is not null, or has a value that is 'truthy'. These can be important things to know. But for our first tests, what we want to know is that the elements are in the document.
Run the First Test- "Red"
We are going to run the first test. It should fail because we haven't written the component it is testing yet. Jest will automatically find the test file when you save it and attempt to run it if you already have Jest running. You might already have Jest running if you started it in the previous post and didn't stop it. This is fine.
If you don't have Jest running already, start it using the command npm test
.
Npm Test
You don't have to understand anything in the next paragraph, but I wanted to give you a short explanation of all the steps that happen when you run the command npm test.
This project was started with create-react-app
. create-react-app
automatically sets up a lot of the stuff that is needed to make a React app work. One of the things that create-react-app
sets up is testing. It sets up the scripts so that all you need to do is type 'npm test' and Jest, our test running program, will find the tests and run them. create-react-app
also sets up Babel. Babel turns React code into JavaScript. Jest only understands JavaScript, so for Jest to test React components the React code has to go through Babel first.
What happens when you run the command npm test
is npm looks in the package.json file in your project directory. It looks inside "scripts" and finds the script named "test." Then npm will run the commands that it finds there. The command that is there by default is "react-scripts test." "react-scripts test" is a set of commands that runs your code through the Babel compiler then tells Jest where to find the tests in your project folder.
Now Run the Test
Type the command npm test
and hit enter.
flashcard> npm test
Jest will tell you that the test suite failed. A test suite is what Jest calls a group of tests in a file. This test suite failed because we haven't written the Answering component yet, so it couldn't load it. Once we make Answering
then the test suite will have something to load. When we write more tests we will see individual tests failing, not a whole suite.
This is the "Red" part of Red, Green, Refactor. You want the test to fail when you first write it. If your test doesn't fail when you write it, that means you are testing for something your app already did. That means you are not testing for a new feature.
Jest Commands and "Watch" mode
Jest is the program that runs the tests that we write. We'll be using Jest a lot. So now is a good time to learn about some of the commands that you give Jest. You started Jest when you ran the command "npm start." By default Jest is set up to go into "watch mode." When Jest is in watch mode it will watch for changes that you make to files. When you change a file and save it, Jest will run your tests again. This saves a lot of time because you don't have to switch to the testing window and type "npm test" every time you want to see your new test results.
You may have noticed this menu of Jest commands.
Watch usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
Typing 'a' will run all tests in the project. Typing 'q' will turn off watch mode. Don't turn off watch mode!
Typing 'p' brings you to a screen where you can run tests from a particular file. Try typing 'p' then type 'A'. See how Jest gives you a selection menu?
Finish typing 'Answer.' Use the arrow keys to select a file. Hit enter to run the tests.
If you want to quit out of Jest without closing the window, hit control + c. This command may be different on Linux and Macs.
Pass Answering Test 1: Has a Container
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-1.tsx
Create a new index file named index.tsx
in the folder src/scenes/Answering/
.
//import React so that we can use JSX
import React from 'react';
//import all the components from Semantic UI React
import {
Button,
Container,
Form,
Header,
TextArea
} from 'semantic-ui-react'
const Answering = () => {
return (
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
</Container>
)};
export default Answering;
The Container. We gave it a testId of 'container' so that the query in the test can find it.
The style prop lets us set CSS on the container. We'll move it slightly away from the left side of the screen. Later in the tutorial we'll add the Selector
menu to the left side of the screen, so we'll need space for that.
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
</Container>
Run the Test Again
If Jest is still in watch mode then it will run automatically when you save your changes to Answering/index.tsx. If Jest is not in watch mode or you quit out of it, run the command npm test
to start it up again.
Now the test passes! We are ready to add the second test.
Answering Test 2: Has a Question
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-3.tsx
Write the second test. The second test gets added to the file that you already started. Do not delete the first test.
//test to see if the question prompt is in the document
it('has a question prompt', () => {
//Use Object Destructuring to get getByTestId from the result of render
const { getByTestId } = render(<Answering/>);
//find question by searching for testId 'question'
const question = getByTestId('question');
//assert that question is in the document
expect(question).toBeInTheDocument();
});
What's going on in this test?
This test looks a lot like the first test that we wrote. Compare it to the first test. Can you identify the name?
The name of this test is 'has a question prompt.'
Do you see where the function that we pass to it()
starts and ends?
Each line in this test is doing the same thing as the same line in the test for the container. The difference is that we call the element variable question
, and we search for a testId 'question'. Then we test the assertion that question
is in the document.
Question Test Fails: Red
The second test fails when you run it.
Look at the top of the command prompt screen where your tests are running. See the HTML that is printed at the top of the screen? That is the HTML that your code turns into. When a test fails, Jest will print the HTML so that you can see what is being rendered. Seeing the HTML gives you a starting point to figure out why your test is failing.
Look at the HTML that Jest printed when this test failed. Can you see the testId of the container? Can you figure out why the test for the question failed?
The test failed because there is no component with a testId 'question' in the HTML that Jest was testing.
Now let's write the code to pass the second test.
Pass Answering Test 2: Has a Question
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-2.tsx
Change the Answering component so it will pass the test looking for a question. Add a Header component. We already imported the Header component from Semantic UI React so you don't need to change the imports for this step. Give the Header component a testId 'question.'
const Answering = () => {
return (
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
<Header data-testid='question'/>
</Container>
)};
This is the Header that will eventually show the question to the user.
<Header data-testid='question'/>
Now the test passes! We are ready to add the third test.
Answering Test 3: Skip Button
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-4.tsx
We want the user to be able to skip to the next card. We'll give them a Button
to do that. We plan to have the button labeled 'Skip.' So we don't have to use getByTestId
to find the button. We can search the document to find the text 'Skip.' We search for text using the getByText
method.
//test to see if the Skip button is in the document
it('has a button to skip the card', () => {
//use Object Destructuring to get getByText from the result of render
const { getByText } = render(<Answering/>);
//find Skip button by searching for string 'Skip'
const skip = getByText('Skip');
//assert that the Skip button is in the document
expect(skip).toBeInTheDocument();
});
getByText is different from getByTestId
. getByTestId
searches for testId
s. getByText
searches for text. getByText
will return a reference to the element that contains the text. We can find the Skip
button by searching for the text string 'Skip' because we will make the Skip
button show the string 'Skip' to the user.
The string 'Skip' that we give to getByText
has to be capitalized because getByText
is case sensitive, and we will capitalize 'Skip' on the Button.
Skip Button Test Fails: Red
Run the test. It will fail because there is no text 'Skip' in the document.
Pass Answering Test 3: Skip Button
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-3.tsx
Change the Answering
component so it will pass the test looking for the Skip
button. Add a Button
component. We already imported the Button
component from Semantic UI React so you don't need to change the imports for this step. Give the Button
content that says 'Skip.'
const Answering = () => {
return (
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
<Header data-testid='question'/>
<Button>Skip</Button>
</Container>
)};
Now the test passes! We are ready to add the fourth test.
Answering Test 4: Has a TextArea
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-5.tsx
Here's the test for the TextArea
where the user will write their answer. You should recognize every part of it from the other tests we have written.
//a textarea to write the answer in
it('has a textArea to type the answer in', () => {
const { getByTestId } = render(<Answering />);
const textArea = getByTestId('textarea');
expect(textArea).toBeInTheDocument();
});
Try to answer these questions:
What is the name of the test?
Where does the anonymous function passed to it() start and end?
What component are we rendering?
What matcher method are we using to find the element inside the component?
What are we calling the element that we search for?
What assertion are we making about the element?
Don't worry if you can't answer some of these questions. You'll pick it up by the end of the tutorial.
Skip Button Test Fails: Red
Pass Answering Test 4: Has a TextArea
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-4.tsx
const Answering = () => {
return (
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
<Header data-testid='question'/>
<Button>Skip</Button>
<Form>
<TextArea data-testid='textarea'/>
</Form>
</Container>
)};
Let's add the TextArea
. We'll need to put it inside a Form
. We need to put the TextArea
inside a Form
because the Semantic UI React TextArea gets some css styling from being inside a Form
. It won't look right without the Form
.
<Form>
<TextArea data-testid='textarea'/>
</Form>
This is the TextArea
where the user can type their answer. We gave it a testId 'textarea' so that the query in the test can find it.
Answering Test 5: Has a Submit Button
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-6.tsx
We want the user to be able to Submit their answer. We'll give them a Button to do that.
//test to see if the Submit button is in the document
it('has a button to submit the answer', () => {
//use Object Destructuring to get getByText from the result of render
const { getByText } = render(<Answering/>);
//find Submit Button by searching for string 'Submit'
const submit = getByText('Submit');
//assert that the Submit button is in the document
expect(submit).toBeInTheDocument();
});
Because the Submit Button will display text, we'll find it using getByText
instead of giving it a testId
.
Submit Button Test Fails: Red
Write the Component To Pass Submit Button Test: Green
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-5.tsx
This is the Submit
button. Eventually we will write code so that clicking this button will make the Answering
component show the answer from the current card to the user.
<Button>Submit</Button>
Add the Submit
button to Answering
.
const Answering = () => {
return (
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
<Header data-testid='question'/>
<Button>Skip</Button>
<Form>
<TextArea data-testid='textarea'/>
</Form>
<Button>Submit</Button>
</Container>
)};
Now the test passes! We are ready to add the last test. The last test will be the snapshot test that we talked about earlier.
The Last Test: Snapshot
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-7.tsx
Now let's write the last test. The last test is the snapshot test.
//and the snapshot
it('Matches Snapshot', () => {
//get the asFragment method so we can look at the component as a DocumentFragment
const { asFragment } = render(<Answering/>);
//expect the result of asFragment() to match the snapshot of this component
expect(asFragment()).toMatchSnapshot();
});
What's going on in this test?
You saw render() in the other tests. render() returns an object with a lot of different methods in it. Most of these methods are different ways of searching through the result of render(). Like the first test, we use Object Destructuring to get a method from the render result. Instead of getting getByTestId, in this test we get the asFragment method from the render result.
asFragment() is the function that we got out of render(<Answering) on the second line. asFragment() returns the code as a DocumentFragment. A DocumentFragment
is some code that can be turned into HTML. We'll be testing that code to see if it matches what we think it should.
toMatchSnapshot()
is a matcher function that checks if the parameter passed to expect matches a snapshot. A snapshot isn't a picture. A snapshot is a stored version of the code that was generated by render()
. This is useful for telling you that the component you got back from render()
still looks the same way it did before, even if you changed the code inside the component.
You don't have a snapshot of Answering
yet. But after you write the component and then run the test, Jest will generate a snapshot. Jest saves the snapshot of a component into a new folder that it makes for keeping snapshots in. If you change the code and the component no longer matches the snapshot, the snapshot test will fail. When a snapshot test fails Jest lets you update the snapshot to match the new code.
Top comments (0)