What do you think of starting 2021 by learning how to test your applications?
Well, my friends, if that is your goal, I have just the thing for you
I'm starting a youtube channel, in which I'm going to release weekly videos about react native, and if you're into react native, you should definitively give it a check.
The tutorial you're about to read is also available in video format right here
YouTube:
Why use integration tests and testing library?
Integration tests
Integration tests are the easiest way to start testing your applications, simply because they are pretty straightforward.
Your only concern should be going to a top level component, rendering it (and consequently it's children components), and then testing it.
Way much easier than working with unit tests, where the steps are trickier, and achieving a good level of reliability in your application requires you to be more experienced in testing, and even though, you can screw up
"So what you are saying is unit tests are bad ?"
Absolutely not
the thing is, in a real world environment (and in an ideal scenario), you're probably going to want all available types of tests in your application.
But if you are reading this tutorial, the probability is you don't know how to test applications yet, and is looking for somewhere to start.
And integration tests are the best way to start testing your applications, I believe in this thanks to the fact that as I've mentioned before, they are super straightforward and it is relatively easy to cover most test cases using them.
And I don't know about you, but when I work with a tool that "simply works" I tend to keep using it. And if you keep learning about tests and testing your applications, sooner or later you're going to realize that you need other types of tests in your application, and why you need them. Until that moment, integration tests are going to keep your application safe enough.
Testing library
Well, you have probably already heard of testing library. It's a library that gained a lot of popularity in recent times, simply because it changed the way the we think about tests, moving the focus from unity tests, which often ended up testing implementation details, to a way of testing that resembled the way users interact with our application
because of these (and many other reasons that can be checked out in testing library's official page
What we are going to test
I've created a super simple todo app for us to test in this application. It's full scope includes:
- Creating items
- Deleting items
- error handling
I really wanted to keep the app as simple as possible, so all the focus of this article went to the testing itself, rather than to have to explain some fancy feature.
This is the app we are going to test:
You can download it here
Installing testing library
In the application we are going to test, I have already installed testing library, but all that you have to do in order to install it is install the testing-library/react-native as a development dependency.
"Development dependency ?"
Yes, everything test-related isn't necessary for production, hence it doesn't need to go to the production build.
you can install it by using one of the following commands:
- Using NPM
npm install --save-dev @testing-library/react-native
- Usign yarn: Write a new post Edit Preview  ChangeEscolher ficheiroNenhum ficheiro selecionado Remove whenever you render a component using testing library's  Upload image
yarn add --dev @testing-library/react-native
Start the testing
When you create a react-native project, a test runner (Jest ) is already configured by default, what means that you don't have to configure anything to get your tests up and running.
All that you have to in order to make jest recognize that you are writing a test, is create a regular fil, but before adding the .js (or tsx) extension to it, you have to add .test or .spec to that file.
I usually like to give my test files the same name as the component they are testing, so, if I was testing the App.js component for instance, My test file would be called App.test.js.
Even though you don't have to use the same name for test files and "tested" files, it makes way easier to keep track of what test refers to what component.
In this application we are going to test the App component
Create a file called App.spec.js (or App.test.js)
and import these files to it
import React from 'react';
import App from './App';
import { render } from '@testing-library/react-native';
We have to import React into our file, because we are going to use jsx in this file, and whenever we are using jsx. That is valid for react versions < 17.x.x. If you are using react version 17 (or higher) JSX transform is going to handle that for you
We have to import the component we are testing (App)
We have to import render from testing library, because that is the method that's going to allow us to interact with our component in our test environment
"But how do we test applications by the way ?"
Testing applications can be simplified to two simple actions
1 - make something inside your application
2 - testing if what you've done generated the expected result
"How does that translate into our code ?"
Let's split this by items
1 - "make something inside your application"
Testing library provides us a lot a helper methods that allow us to interact with our react application, basically the ways in which we can interact with our component are split into 2 categories:
- searching for something inside our application
- interacting with our application
and we can do both of them using testing library, but I'll cover that in more detail in the next steps of this tutorial
"2 - testing if what you've done generated the expected result"
That's out of testing library jurisdiction. The tool that handles this for us is jest. How this is achieved is by using Matchers
" Jest uses "matchers" to let you test values in different ways. " - Jest's Docs
I know it all sounds a little abstract, but making a long story short, matchers are a way to compare the values your test has generated to the values you expect
"Ok, I already know how to test stuff on an abstract level, but what should I look for when testing ?"
well, testing is all about making your applications reliable, so you should test if your application works. That means that the question you should always ask yourself when testing something is
"How should this work ? what should this application do, and what shouldn't it do?"
whatever the answer to that question is, that is what you should be testing in your application.
Too abstract for you ? ok, let's start testing your application, and hopefully things will become clearer for you
Defining the tests of our application
Basically this application has 5 tests that will ensure the application's functionality
- The capability to create one item
- The capability to create multiple items
- The capability to delete one item
- Test if the error warning appears when the user try to create an item with invalid data
- Test if the shown error disappears after one valid item is created
So let's create these tests one by one
1 - Creating one item
Before we start testing, we have to know the steps we are going through to fulfill our test.
The necessary steps to create a new item in my application are:
- Find the text input
- Type something into the text input
- Click the button with a plus sign on it
so that is what we are going to do in this specific test. And after following these steps, all that we have to do is check if the taken actions generated the expected result
1.1 finding the text input
The first step to test our application, is to first render the component, so we can interact with it, so let's do it on our code
import React from 'react';
import App from './App';
import { render } from '@testing-library/react-native';
it('Should create an item', () => {
const { getByText , getByPlaceholderText} = render(<App />);
})
in this code snippet, we have two new "items". One of them is the it function that wraps our test
basically it is the method which runs our test for us.
it requires two parameters, the first one is
"But where does it come from ?"
as you might have noticed, we do not import it anywhere. That is possible because it (and many other methods) is globally available in our test environment. You can read more about jest's globals here
the second thing you probably isn't familiar with, are the getByTextgetByText and getByPlaceholderText methods.
The render method returns us a lot of methods, which can be used by us to inspect the rendered application, and serve this purpose.
Testing library is engineered to make us test applications in the same way our users are going to consume them. And that explains the naming of the 2 functions returned by render
Finding the text input
Usually, the way used to find text inputs using test library, is querying our application, searching for it's placeholder text.
So let's find it in our application, and assign it to a variable
import React from 'react';
import App from './App';
import { render, fireEvent } from '@testing-library/react-native';
it('Should create an item', () => {
const { getByText , getByPlaceholderText} = render(<App />);
const textInput = getByPlaceholderText('Write something');
}
Finding the "add item button"
Buttons don't have placeholder texts, so we cannot use the same method used to find the text input when querying for our button.
But there's a text element inside our button, and we can use it to find the button's value, and assign it to a variable
the method used to do so is: getByText
import React from 'react';
import App from './App';
import { render, fireEvent } from '@testing-library/react-native';
it('Should create an item', () => {
const { getByText , getByPlaceholderText} = render(<App />);
const textInput = getByPlaceholderText('Write something');
const addItemButton = getByText('+');
})
Interacting with our components
Now that we have successfully queried and assigned our components to variables, it's time for us to interact with them.
The way we are going to accomplish this, is by using testing library's method fireEvent
We want to do 2 things. type some text into textInput, and press addItemButton. ]
it('Should create an item', () => {
const { getByText , getByPlaceholderText} = render(<App />);
const textInput = getByPlaceholderText('Write something');
const addItemButton = getByText('+');
const createdItemText = 'first todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
})
at this point, our item should already have been created. But we cannot be sure, because there is nothing to confirm that for us.
to confirm that our item has been created we must query our rendered component looking for it, and then use a matcher to make sure that it exists
import React from 'react';
import App from './App';
import { render, fireEvent } from '@testing-library/react-native';
it('Should create an item', () => {
const { getByText , getByPlaceholderText} = render(<App />);
const textInput = getByPlaceholderText('Write something');
const addItemButton = getByText('+');
const createdItemText = 'first todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
const createdItem = getByText(createdItemText);
expect(createdItem).not.toBeNull();
})
this is what the finished code of our first test looks like.
we used one of jest's globals, expect
expect receives a value and has matchers as submethods. Remember when I told you that tests were all about doing something and checking if our actions generated the expected results?
expect is what's going to check the result for us
we used 2 matchers with expect in this test.
the first one is
not
It's a simple negation, it can be translated to javascript's ! operator.
The second one is
toBeNull
which evaluates if the value you passed to expect, is a null value.
when they're combined, our matcher is going to accept any value that isn't null
you can now open your terminal, and run your test using the following command :
yarn test --watch
The --watch flag is going to keep your process running, and listening for changes in your tests, and is going to run the ones that suffered changes. Alternatively if you want to run all tests every time a change occurs, you can use --watchAll instead of --watch
We have just successfully written our application's first test.
Testing for false positives
one of testing's golden rules is to never trust a test you have not seen failing.
It's important to make our tests fail on purpose, so we can make sure that we aren't getting false positives from our tests.
So if you are coding along with this tutorial, change something that is going to break your test and check (search for a text that shouldn't exist on the screen, comment one step of the process, etc) if it really breaks your test's pipeline.
2- Creating multiple items
This test, is going to be very similar to the first one. The only difference, is that we are going to repeat some steps multiple times.
Since we aren't going to do anything new here, I'm going to show you the code right away
it('Should create multiple items', () => {
const { getByText , getByPlaceholderText} = render(<App />);
const addItemButton = getByText('+');
const textInput = getByPlaceholderText('Write something');
const createdItemText = 'first todo';
const createdItemText_2 = 'second todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
fireEvent.changeText(textInput, createdItemText_2);
fireEvent.press(addItemButton);
const firstCreatedItem = getByText(createdItemText);
const secondCreatedItem = getByText(createdItemText_2);
expect(firstCreatedItem).not.toBeNull();
expect(secondCreatedItem).not.toBeNull();
})
3 - The capability to delete one item
Before deleting one item, we first have to create it, so we are going to reuse the item creation code we have created for the first test, search for the X Text to find the delete item button, and then checking if the item disappeared
it('Should delete an item', () => {
const { getByText , getByPlaceholderText } = render(<App />);
const addItemButton = getByText('+');
const textInput = getByPlaceholderText('Write something');
const createdItemText = 'first todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
const deleteItemButton = getByText('X');
fireEvent.press(deleteItemButton);
})
Note that we don't make tests to make sure that our item has been created, that has already been tested, hence, there's no need to test it again
Up until this point, we have found our delete button and pressed it. Now we just have to test if the created item disappeared
"I got this, we have to use getByText, search for the text of the created item, and use a matcher to make sure it's value is null, right ?"
Almost right
The only thing we will do differently is, instead of using getByText, we are going to use queryByText
But Why ?
well, the thing is, whenever we search for a text that doesn't exist in our rendered component using getByText, it throws us the following error
but thankfully for us, this can be easily solved by using query by text.
And adding the deletion verification, our test is going to look like this
it('Should delete an item', () => {
const { getByText , getByPlaceholderText, queryByText } = render(<App />);
const addItemButton = getByText('+');
const textInput = getByPlaceholderText('Write something');
const createdItemText = 'first todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
const deleteItemButton = getByText('X');
fireEvent.press(deleteItemButton);
const deletedItem = queryByText(createdItemText);
expect(deletedItem).toBeNull();
})
now our test is going to pass and work just as expected
4- Test if the error warning appears when the user try to create an item with invalid data
This error occurs when we try to create an item without text in it.
So to test it, we need to press the add item button without changing the text input, and then verify if the "Please insert a valid text" error is shown on our screen.
There's nothing new, in this test,we're going to use the same methods we've been using until this point
it('Should display an error when trying to create an item without any text', () => {
const { getByText } = render(<App />);
const addItemButton = getByText('+');
fireEvent.press(addItemButton);
const errorMessage = getByText('Please insert a valid text');
expect(errorMessage).not.toBeNull();
})
5- Test if the shown error disappears after one valid item is created
This last test is going to be a combination of test n°4 and then test n°1
The first step is to make the error appear
it('Should remove the error message after creating a valid item', () => {
const { getByText } = render(<App />);
const addItemButton = getByText('+');
fireEvent.press(addItemButton);
})
we don't need to make sure the error is being shown because have just tested this in the last test.
and then, create one item, and finally, make sure that the error message isn't present in our test
it('Should remove the error message after creating a valid item', () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
const addItemButton = getByText('+');
fireEvent.press(addItemButton);
const textInput = getByPlaceholderText('Write something');
const createdItemText = 'first todo';
fireEvent.changeText(textInput, createdItemText);
fireEvent.press(addItemButton);
const errorMessage = queryByText('Please insert a valid text');
expect(errorMessage).toBeNull();
})
and that finishes our last test.
🎉🎉🎉🎉🎉🎉🎉 Congratulations 🎉🎉🎉🎉🎉🎉
We have just tested "an entire application" together
I hope this tutorial helped you understand tests a little better, and realize how straightforward testing with testing library is
If you enjoyed this tutorial, please consider subscribing to my youtube channel :)
Top comments (1)
Solved my issues