Creating a simple counter React Native app using Test-Driven Development

Doan Andreas N.
14 min readJun 6, 2021

--

Real developers ship. And to ship quality software, we need to make sure it works correctly and matches the product requirements.

How can we achieve that? One way is to hire QA testers to test it manually, click this button, fill this form, buy this, etc. However, this process can take up so much time for large and complex applications and is not a very efficient testing method.

While manual testing is not going away, we can make the testing process more efficient with automated testing. We write code that keeps tabs of our application code — making sure it works correctly.

Many people (including me) were initially quite confused about how to test front-end applications. This tutorial will discuss why testing front-end UI is essential and valuable while also dismissing the myth that testing wastes time and effort.

I will also show how to test UI code in React Native, using Jest + React Native Testing Library. As conclusion to this article, I will also talk about how testing has helped me developing production React Native applications.

Why testing UI is important

We all have asked or been asked this question at least once while working on front-end. And until recent years, the term unit testing was not very popular among FE developers. Here are some of the reasons why we should write unit tests for FE code:

It lets you capture bugs before the QA team does

We all know that it is difficult for a QA and a dev to live in harmony. Devs do not like it when the QA team finds bugs in their work and tells them to make changes. If your code has a good test coverage, there are fewer chances of finding bugs. That’s a win-win situation for both sides right?

Helps other devs (and yourself!) understand your code better

All the test frameworks have a describe block where you define what the method is supposed to do, just how you would write comments for your code.

Tests can teach you about your code (or others) in unexpected ways (edge cases), and also make it easier to debug.

Refactors your code

You will start asking these questions to yourself while coding: How will I test this code?, How do I make sure that each method I write can be tested? If you already ask these questions while writing code, then you are among a small minority of developers.

Makes your code modular

If your code is tested, there are 99% chances that it is modular, which means that you can easily implement changes in the future.

Makes debugging/implementing changes much, much easier

There will be cases when you will be required to change a function’s logic, e.g., a currency formatter function which returns a string. One way of debugging it would be to go through the UI and check if the desired output is showing up. Another smart way would be to fire your test cases, change the test results according to your desired output and let the test fail. Now change the logic inside your method to make the failed test pass.

Does NOT increase development time

Many of us give the excuse that writing test cases will increase the development time (even I was of the same opinion). But believe me, it doesn’t. During the course of development, almost half of the time is spent in debugging/bug fixing. Writing unit tests can decrease this time from 50% to less than 20%.

Hopefully now I have convinced you that testing is very useful! We will finally get to the exciting part: learning how to actually write tests (in React Native).

Testing a basic counter app in React Native

Yeah, the tired cliché of very basic React/React Native tutorial: counters.

Most of us have learned to implement a simple counter when we first learned how to use React (or React Native). Definitely a smaller number of us, however, has learned how to test it!

In this tutorial, we will initialize and configure everything from the very start. Here are the list of what we’re going to do:

  • Configure Jest & React Native Testing Library in Expo;
  • Learn quick intro to Test-Driven Development (TDD);
  • Creating test for our counter application (using TDD method);
  • (Bonus) Install Wallaby.js and configuring it.

NOTE: You can watch the video edition of this tutorial here:

Initializing Expo project

I assume you already have Expo in your computer. If not, then you can run the following command on your command prompt (or terminal):

npm install -g expo-cli

Wait for a while… and after it finished, we can go on to create a blank Expo template. Open terminal in your favorite working directory, then run:

expo init ReactNativeTDD

ReactNativeTDD will be the name of our app. You can name it anything you like, but I will go with that name.

When asked to choose a template, just choose anything you want from Managed Workflow (not the Base one). Although I’m going to use TypeScript, you can follow along using only vanilla JS.

Your terminal will look like this when the project is ready. Follow the instructions to start your app. If you have difficulty doing it, try following the official documentation.

Try running the app in an emulator (or directly in your phone). If your app can run, you can go to the next step.

Installing Jest and React Native Testing Library

Open our fresh Expo project in your favorite text editor. The contents should look like this:

Open terminal in your project’s directory and run:

yarn add -D jest @testing-library/react-native @testing-library/jest-native react-test-renderer @types/jest

This will install Jest, together with React Native Testing Library (RNTL). React Test Renderer is a peer dependency of RNTL (it won’t work without Test Renderer), so you should install it too. We’re using Typescript, so we will install Jest type definitions, so that VS Code linter works.

Note: If you’re using JS, you don’t have to install @types/jest.

Your package.json should now look like this (pay attention to devDependencies):

If you’re using Expo JS template, don’t worry about @types and typescript dependencies — other than these deps, it should look the same.

Configuring Jest

Now we’ve installed both of our test dependencies, let’s configure them to work!

Create a new file called jest.config.js in your project’s root directory. If you don’t know what that means, it’s where the package.json is also located. The contents should look like this:

It will configure Jest to use React Native preset.

Optionally, if you’re using VS Code, you can add typeAcquisition to tsconfig.json, in order to add autocomplete functionality. Like this:

If you’re using JS instead, create a new file called jsconfig.json in your root directory. The content should be:

Finally, add test script to package.json, so we can run our test. Like so:

What are we doing in our test script?

  • jest: runs the Jest test suite. It will look for tests in our project, and runs it.
  • --watchAll: Jest will watch for changes in our app (or our tests), and run it every time we save changes. Makes testing easier, because we don’t have to re-run it manually every time we add code.
  • --verbose: add more information to test output.

Hooray! Everything is ready and configured. Let’s jump on the most exciting part: writing the test itself!

Creating an example test

Create a new folder in your project’s root directory, called src (for our source code). Add two empty files: Counter.tsx, and Counter.test.tsx (if you’re using JS, then use .jsx as the file extension). Now, your project should look like this:

As I’ve said before, Jest will look for tests in our project. And how did it do that? Files ending with .spec or .test are going to be treated as tests by Jest. That’s why our Counter test ends with .test.tsx.

Jest looks for files ending with spec or test. (Credits: Fireship — Software Testing in 100 Seconds)

Try running it with yarn test (or npm run test if you prefer). You should get:

Jest is angry because our test file is empty 😡

Hooray! We can confirm Jest is now working. To make it happy, let’s add an example test. Edit our empty Counter.test.tsx, and add the following:

Save it, and…

Jest is working, and we can go on to creating our Counter component!

Magic! Finally, our first passing test! Now, let’s review the test we’ve added:

  • describe is a function that describes what it is that I’m testing. To be precise, describe defines a test suite. We want to test our Counter component, so we name it Counter.
  • it is a function for individual tests. One describe can have multiple it inside. Our unit tests is inside this function.
  • expect is a function to assert expectations. We expect the value of 1 + 1 to equals 2.

(Finally) creating our Counter component

We want our Counter component to:

  • Renders correctly
  • Show an initial state of zero counter
  • Increments counter by one each time increment button is pressed
  • Decrements counter by one each time decrement button is pressed

Let’s create these tests!

Jest will be angry, and the Counter test suite should fail. However, we can’t really create any passing tests yet, because we haven’t even implemented our Counter component. For now, let’s make these tests as to-do:

Save it, and…

We can use todo in Jest to mark tests we haven’t implemented yet.

Now our test passes! We can go on to implement these tests one by one.

Quick Intro to Test-Driven Development

Red-Green-Refactor cycle (Yes, I know the color is wrong). (Credits: Fireship-Software Testing in 100 Seconds)

In Test-Driven Development (TDD), there are three basic steps:

  • Red: create a failing test, then
  • Green: create application code (as minimal as possible) that passes the test, finally
  • Refactor (optional): rewrite the application code (if needed) without changing its functionality.

I am not going to extensively talk about TDD in here, but you can check Obey the Testing Goat for further learning (it’s a great material for TDD, unfortunately it’s using Django and not React Native. The principles still transfer though).

How can we apply this TDD cycle to our app?

  1. Create failing tests. Try to render a Counter component that doesn’t exists yet — that will obviously fail.
  2. Create minimal application code that passes the test. Create a very minimum React component that can render correctly.

There goes our first Red-Green-Refactor cycle! Let’s actually implement it.

Implement a (basic) Counter component

Add imports to our Counter test. While we’re at it, let’s try implement the “renders correctly” test:

Try to run it, and it should fail. This is expected fail (Red), because we haven’t actually implemented any Counter component yet. Let’s create a basic Counter component, just to have it pass this test:

Note: if you use JS, don’t import FC. Just type const Counter = …

Our component can render correctly (Green)!

What does render do? It renders the component (duh), and returns a set of functions we can use to test the rendered component. More on the “set of functions” later.

Counter shows initial state of zero

Let’s implement our second test: Counter should display counter of zero.

queryByText will get a children component that matches a specific test. And if RN Testing Library can’t match anything, they will return null. If multiple components matches, it will throw an error (so the text should be unique). If you want to query multiple components, use queryAllByText.

If you want to know more about queries, you can read the RN Testing Library documentation. For now, just know that we will get one element with “Counter: 0” in it.

We expect that we will get one, which means we won’t get null. So, we write a test that verifies that there exist an initial state of 0, and it can’t be null.

Try running the test… and it should fail. Again, this is an expected failure (Red). Let’s try to pass the test with minimal changes:

Save it, and…

Although hardcoded, our component shows the correct initial state of 0

Way to go! We have successfully confirmed that the initial state of 0 exists.

Implement increment functionality

We want to test interactivity (pressing a button). How can we do that? By using fireEvent. We can use it to press something, for example a button (More on fireEvent at the documentation).

getByTestId is very similar to queryByText, but it looks for a component that matches the given test ID props. More on test ID later. The most important distinction between get and query, is that get throws an error if it can’t find a matching element. This is useful if you want to guarantee the query result shouldn’t be null.

For now, save the test, and it should fail.

Pay attention to the error: it was readable and easy to understand in a glance.

Why should we use get then, if it does the exact same thing as query? Just query the element and check if it was null, no?

Well, you can do that, but by using get we will get a nice, descriptive error: Unable to find an element with testID: incrementButton.

Using query instead of get.

This one does the exact same thing, but we don’t get the nice bonus of a descriptive error message.

Let’s move on to implement our increment button:

We give it a testID props, so that getByTestId can query the button. It won’t do anything inside our application, it’s just used to identify an element inside a test. You can go ahead and name the ID anything you want, but try to make it descriptive.

Save it, and…

Tests helps us to avoid regression in our code

Jest gets really angry now… Wonder why?

Oh no! We broke our entire app? What just happened? Looks like our component can’t even render correctly, so we know why the other tests fails: because it can’t render.

This is why tests is useful: it makes sure our app doesn’t get broken because of some bugs. Every time we run our tests, we make sure what already went right before don’t get broken because of new functionality.

If we look carefully, a React Native Button has two required props: title and onPress. Let’s add those then:

Save it, and…

Our other tests finally runs correctly!

We fixed the problem. Although the test still fails, we have progressed forward: we can get a button (using getByTestId) and press it (we can see the console.log message).

Let’s implement a counter state inside of our component:

Save it, and…

Only one test to go!

Voila! It passes!

Exercise: implement the decrement button test

As of now, we already learned:

  • TDD cycle (Red-Green-Refactor);
  • How to render a React component;
  • How to query elements from a rendered component; and
  • How to interact (press) a button through a test.

Now, it’s your turn to try it yourself!

  1. Implement the decrement test. It should be similar to our increment test.
  2. After you get a failing test (Red), implement the actual decrement button.
  3. Then run your test, and hopefully the test should pass (Green).

If you are stuck, you can check the solution in this Github repo.

Integrating with App component

Now that we know our Counter component fully works, we can integrate it with our main App component.

Try to run it on your emulator/phone, and…

Works like a charm, even though this is the first time we actually run our counter!

The styling is a bit off, but eh, it’s not the focus of this tutorial. I’ll leave it as a bonus if you want.

You can check the final result in here:

Bonus: Configure Wallaby.js for VS Code

All this time, we’re looking for test errors in our terminal. It does the job, but Wallaby.js can offer you a more satisfying test experience!

If you’re using VS Code, install the Wallaby.js extension.

Click the Extensions button in the sidebar, search for “wallaby”, and install it.

In order to run Wallaby.js, we should use a configuration. But don’t worry, they will do the heavy lifting for us — we don’t have to type in anything.

Use the shortcut Ctrl + Shift + P, type in “wallaby select configuration”, click on it, then choose the Automatic Configuration <project directory>. After a while of loading, you should see green boxes besides our tests.

Wallaby.js displays error right inside our code, making it much more readable.

Try to tweak your tests! Similar to Jest, we should save the file first in order to see the test changes.

Wallaby is amazing. However, if you use the free version, Wallaby will go idle after a few minutes of not using it, and you have to restart VS Code to use it again. If your team can afford it, I think it’s a handy tool to make testing more productive.

How tests have saved me in the past

Remember my point about “tests helps us avoid regression”? And that tests helps us debug faster? Also that tests makes it easier to maintain our codebase? I have experienced these benefits myself.

Me and my team of five people are currently working in a software engineering group project from our campus. We use React Native as our front-end stack. We are required to use TDD methodology in developing our software, and we need to have at least 90% code coverage. We hate testing before, but we just don’t have any other option…

So we learned how to create tests.

In the first and second sprint, I still feel writing tests is cumbersome. “It’s just a waste of time”, I thought.

Tests assures that a huge codebase works correctly

There’s a goddamn lot of scenes and components to maintain, alright…

Although, as our codebase continues to grow, I feel like our tests assures us that everything and every component is working, that we don’t break something with a new, weird bug.

This much test assures us that each and every component works as intended.

Tests help me debug that I forgot to add environment variables

Environment variables for our app.

One time I was migrating my Windows to a new SSD, and I have to re-clone our repository. However, I forgot to create environment variables.

Out of habit, every time I pull a new update from our Gitlab repo, I will run tests first to ensure everything works.

But I got some errors, that says (in a nutshell) “environment variables are undefined”.

Imagine if I don’t run test first, and jump straight to running the app! We all know React Native stack trace are a nightmare, and I probably will end up throwing up seeing these cryptic error messages. But these tests caught it early, and I can easily debug what’s wrong.

--

--