3 Strategies to Test Your React App

Introduction For the very beginners about to start testing

3 Strategies to Test Your React App

Testing is an unclear topic for developers at first.

When I noticed I need to test my React project, I had these questions:

  • What exactly should I test? All of the functions? React components? Hooks?
  • Which testing libraries should I use? I heard about jest or something but I don't know how to use 'em in React.
  • How much time do I have to take to write tests?
  • Do I need to apply specific testing methodologies like TDD or BDD? Which I don't know much about.

But as I investigated testing and applied them to my React projects, it really benefited me. Also, I noticed that I can take different - mostly 3 - strategies to test the app.

Testing is as important as building your app. The only problem is the complexity of testing methodologies. So I will give you some introductions about testing, especially on React projects.

Why You Need Tests on Your React Project

When I was junior dev, actually I couldn't see the point of testing. The app is already built and working fine, it's obvious I can see that, then why bother??

But I eventually noticed that testing your app is as important as building it. Testing is not about "Just see if it works", but about "Ensuring the app's quality".

Application is supposed to be used in the 'long run'. That means you need to ensure the app still working even after 1 or 3 years later.

So it's not enough if the app is working right now. When you built the app, it's obvious that should work. But it possibly gets broken after some changes.

Tests Prevent Your App From Getting Broken

You don't want situations like "add a new feature to break something else". And you notice some broken features after it's in production. What a disaster.

But tests can help you notice when something is broken in your app.

Well, you can still check the app manually after each deployment, but it's so painful if the app is large, and you can hardly check the whole app.

If you have tests, the process is very easy every time. Run tests. Passed. OK. Deploy.

Test Make Your Team Know More about the Specifications

Testing is not only about insurance. Writing tests is writing out specifications.

Test scripts make you know "what the app should do". It becomes more clear once you wrote the test suites.

This is important especially when you are working in a team. Your teammate sees test cases and knows what it should do. So he/she can change the implementations or add another feature with confidence.

But without tests, you can't have the confidence to change the codebase. So please, write tests, for others.

3 Levels of Test

Let me talk about what I wanted to know earlier as a beginner.

Generally speaking, there are three kinds of tests:

  • Unit test
  • Integration test
  • End-to-End(e2e) test

These are differentiated by their role, precisely, the testing scope.

For example, a unit test examines the smaller scope of things (like each function), on the other hand, an E2E test examines the consecutive users' interactions.

And it's often described as the "Testing Pyramid" like below.

testing.jpeg

In a nutshell, the upper level of tests has more value for each test case, but it takes much more time and effort than lower levels of tests. So in a typical scenario, the team would have more unit/integration tests and fewer (or no) E2E tests.

Now let's elaborate more on each testing method.

Unit Test

Unit tests examine the smaller scope of things, i.e. individual testable units. It includes each function, class method, or custom hook in React.

Every code base is a set of units. And unit tests ensure each unit can be responsible for its part. This is to inject the bugs in the early stages of development so that you & your team won't be messed up with "something wrong somewhere in the code base" later on.

Hopefully, each implementation should come with its own unit test. Here, test and implementation are 1 to 1 relations, like twins. And this philosophy ultimately is TDD (Test-Driven-Development) methodology. In TDD, developers write unit tests first and then write the actual implementations.

But sometimes it's hard to separate the testing unit individually, for example, React components that work under the context API. In this case (and it's often the case with React App IMO), integration tests may be appropriate.

Integration Test

Sometimes you need to test the application parts in combined scopes. Integration tests examine if the sets of units work as expected.

React components often be tested with other components or hooks. For example, you would test the component's rendering behavior containing some logic other than the component itself:

  • hooks
  • reducers
  • Context API
  • API request to the backend
  • react-router-dom's path variables

It may be running multiple "units", but in this case, the test's desired result is something like one that says "When this occurs, (through hooks and APIs) it should show Foo in the dom". Multiple things, and one result. It doesn't care about the unit's implementations but rather cares about the result. Each unit's implementation doesn't matter. So it is often described as "black-box" (or gray-box) testing.

In most cases with React apps, you write integration tests most often. But sometimes you wanna test it in the consecutive UX as a whole. And it is out of the scope of integration tests. That's where e2e tests come in.

End-to-End(e2e) Test

In some cases, it's painful to test as separated units or integrated parts. For example, test scenarios like "create an item as a logged-in user, leave the page, and then come back to see that item". This is real situation when the users interact with the app through the browsers. And if it fails, the app is useless.

Yes, you can test this kind of thing manually on the browser, but it's hard to test it throughout the app every time.

E2E tests are intended to do this automatically. By using libraries like Selenium or Cypress, it mimics users' interactions on the "actual" browsers. So you can ensure the app is finally can be on production. This certificate can't be obtained even if the units or integrated parts work correctly. It sometimes breaks finally in combined situations of the real world.

The drawback of e2e tests is, that you have to take much more time & effort than writing unit/integration tests. Yes, it's actually hard. Because it requires more control over the user's actions, and besides, because of the latency of the browsers' rendering, timing issues occur (it's typical that the test-runner complains when it can't find the element immediately).

But given all of this, e2e tests are powerful and can be the final assurance of the quality of your app. How much it should be covered by e2e tests, would be discussed with your team considering the ROI.

3 Strategies to Test Your React App

Given all 3 methods above, you can choose how to test your React app. You should not rely on only one testing method, and it is common to mix different testing and make your "Test Portfolio".

Then how do you pick the different testing? What is the best approach to test my project? It really depends on your project, but I would consider 3 strategies I'll explain below.

1. Mostly Integration (with built-in React-Testing-Library)

This is the most typical (& easy to apply) scenario.

If you built your react app with create-react-app, it comes with the built-in jest & react-testing-library combo.

With these powerful tools, you can write & run your unit/integration tests easily. It's go-to when testing React apps.

What is jest & React Testing Library?

Jest is a test-runner library. And you can run all of the tests in the repo by running jest command. It detects all the test scripts named like 'Some.test.js' and runs all the scripts found. Finally shows the result.

React Testing Library is kind of a helper library for testing React components. It comes with lots of useful APIs that can help you find the elements on the React DOM. You import this library and use it on each test script, then run it through jest.

With the power of RTL, you can test your React components in a combined context, for example,

  • render components under Context API so that you can check if the component works when the context changes its state.
  • render components with other components as children, and you don't have to write too many unit tests (when you know unit tests can be too much)

So RTL is mostly for integration tests. This is actually what Kent.C.Dodds (author of RTL) suggests.

In most cases, you can cover what you want to test by integration tests, but sometimes you may want some E2E tests. And it's not impossible to write e2e tests by rendering the whole app in the test dom using RTL. But I'd go with other UI libraries like Selenium for e2e tests.

2. Test critical scenarios with E2E, then cover with unit/integration

Sometimes it's better to test it with e2e tests, like when:

  • Critical user's scenario contains lots of transitions between pages (e.g. Tutorials for the first login of a user)
  • One feature affects the other feature and users are supposed to see them (e.g. Notifications on other users' actions)
  • Your project is already full of legacy codes and it's hard to separate testable "units".

Basically, you can test anything you want, as you do it manually on the actual browser, but it's not practical to test all the features by e2e tests. This is because of the following two reasons.

First, it takes a lot of time & effort to write complete e2e tests. You have to test the whole scenario, so you should pick up all the elements you need in test suites, and it is sometimes really hard. Find an element in the dom, wait for the element to appear, and act on them. It really drains you.

Second, e2e tests tend to become non-deteministic tests. This is the tests like "sometimes passes, but sometimes fails". And it is often the case with e2e tests, because of the runner's rendering latency. In my experience, Selenium's browser driver can be sometimes slow at rendering heavy dom and it takes 10 or 20 seconds to render your app (it depends though), and browser driver complains it can't find the desired element. So the test fails.

And what's worse, your team eventually ignore the test result. Even if it's detecting the underlying bugs. Then what's the worth of the tests?

So that's why it's not practical to totally rely on e2e tests. E2E have their role and you should limit the scope of the e2e tests for the critical scenarios of users. Start from minimum amount of (but critical) e2e tests, then cover the rest of the case with unit/integrations.

Which libraries should I use for e2e tests?

Libraries like Selenium or Cypress is used for e2e tests. This is because they can act on the actual browsers. So you can test the app's actual behaviors on the browser, just like you manually test it. These libraries are not coming by default like React Testing Library, so you need to install it.

Typically you would set up jest as test runner, and use Selenium as browser driver. You can also replace these libraries with mocha as test runner, and Cypress or Puppeteer as browser driver. The choice is yours.

In general testing jest is enough and sufficient, but in a larger projects where you need more customizations over the testing, it's more handy using mocha.

Selenium would be good for the developers with other language backgrounds, since Selenium provides multiple language versions as well as node.js (I've used to written Selenium scripts in Python, so Selenium is my go-to even for React projects).

3. TDD - write code & test in a small iteration

Finally let's talk about TDD - I sometimes use this strategy. Especially when writing new logics from scratch.

In TDD(Test-Driven-Development), you write test first, then run tests, of course it fails, then write the actual code, then run tests again, it passes, then maybe refactor.

What's good about TDD? Imagine you have to implement something like useFilter(). And you still don't have a clear idea on by which field it should filter items. So you write tests, to grab the idea on what you are going to implement, like so:

  describe("filterItems", () => {

    describe('filter eq values on relation id', () => {

      it("should return whole items when no filter provided", () =>{
        // test codes
      })

      it("should return items that matches relation id", () => {
        //
      })
    })

    describe("filter by priceSell", () => {
      it("should return items within range", () => {
        //
      })
    })

    describe("filter by date string", () => {
      it("should return items within date range", () => {
        //
      })
    })

    describe("multiple filter", () => {
      it("should filter as AND condition for multiple field", () => {
        //
      })
    })
  })

This test suites are actually what I recently wrote before implementing the hooks. By writing test cases like this, you can grab the clear idea on what should be implemented.

Also, TDD encourages you to separate the logics so that it can be easily tested. This can result to sparse & cleaner code base.

TDD-style unit tests removes your effort on writing tests, because you don't need to remember what you implemented before (I tend to forget what I wrote 2 weeks ago).

Though it can benefit you like this, but applying TDD everywhere can be "too much" when writing easy stuffs and may slow down your iteration. So it's more practical to apply TDD when it makes sense. Cover it up with integration/e2e tests.

Conclusion

Now you should know what is all about testing React projects.

Remember there are 3 testing levels and you typically start by writing unit/integration tests using the default packages (jest & react testing library). Depending on your project, you may also start by e2e tests or if you are writing from scratch I recommend you to write codes using TDD.

Note this article is intended to describe the very basic concepts of testing on React. Some points are omitted and may be simplified too much.

For more about the testing concepts, refer to these great articles:

In the next article, I will show you how to start writing actual tests as tutorial.