Published on

Front-End Testing for Dummies™ - React Native

11 min read
Authors
  • avatar
    Name
    Asfiolitha Wilmarani
    Trakteer
test driven development frontend testing for dummies react native

It's my first project with React Native and I had to do it the TDD way. How did I do it? Here's front-end testing for dummies™ (such as myself). This post is more like a log of what and how I did my front-end testing for react native.

How Does It Work?

tdd-cycle

Diagram from bitbar.com

TDD is short for Test Driven Development. I first heard about this term in my freshman year during Web Development course. Essentially, developments using this approach goes through three stages of a cycle1. As the diagram above illustrates:

  • Red – you write test first before implementing anything else and run the test. Alas, the test fails and your pipeline is red.
  • Green – you implement the code that lets you pass the tests from the red stage. You pipeline passes and is now green.
  • Refactor – the code you wrote during the green stage may not be the prettiest, hence this is the stage where you give it a make-over. The pipeline during this stage should also be green.

Rinse and repeat for every feature you're adding to your project.

Why TDD?

The course demands us to use TDD in our project(s). Otherwise, I don't really have any good reason as to why we're using TDD. I think the same case applies to my team members as well. I never made tests for front-end-only projects except for one time (that was using snapshots without functional testing).

After I did some reading, I managed to find some advantages to TDD that I can relate to and applies to this project we're working on. Let me list them here.

Aids in Case Handling

Creating tests makes me consider how my components will be used. It makes me list all the possible business cases, including edge-cases. This way, it's easier to keep this case in mind when coding the implementation–as my implementation should cover these cases I defined and these cases only2. Tests can also act as a documentation of the cases that needs to be handled.

Gives Confidence in Delivery

Having tests should give me confidence that my code is working as intended. Every time the pipeline goes green, I can be assured that the new feature I just pushed is working properly. Note that green pipelines will not mean anything if my tests are not relevant, therefore the process of designing tests needs to take a decent amount of effort.

Ensures App Reliability

This one is similar to my point above. Tests are the best way to ensure app reliability–meaning that passing tests ensures that everything should work as intended. I'm making a UI component that needs functional testing. The library I'm using helps me mimic user interactions, firing events as if a user is interacting with said component. Keeping this in mind, I can ensure that if my tests are passing, actual user interactions are handled properly3.

What Tests?

There are several kinds of tests to use for a project. In my case this time, I used functional testing for my component. These are the other tests to choose from4.

  • Unit testing – tests individual modules of application in isolation, without interaction with dependencies.
  • Integration testing – tests if different modules are interacting just as intended when combined together.
  • Functional testing – tests a slice of functionality in the system that may interact with dependencies to confirm that it is functioning properly.

As we can see, for a single UI component like the one I’m making right now, functional test is the most suitable.

My Take on TDD

Honestly, my first impression of this approach was that it's dreadful. I had to code way more than what was needed to deliver the assignment. I mean, it's not like the tests I wrote add to the code I wrote for the assignment. Meanwhile, the lecturers wanted me to have 100% coverage for my codes.

We won't be talking about coverage here because it's an entirely different topic. 😄

Anyway, I had to raise my coverage to 100%. Funny thing is, I kinda enjoyed the process of making tests back in my sophomore year. It's like working towards a progress bar where the 100% coverage is the ultimate goal. I like watching the lines go green one by one as I write more tests.

Though, of course, what I was doing back then was Development Driven Test instead of Test Driven Development. =^=

In this sprint, I'm in charge of making the bottom tab navigation bar for Ajaib on DEX. Here's my process doing TDD with React Native. Bear in mind that this is my first experience using React Native, as well as my first using TDD with it.

List your business cases

First thing I did was I listed all the business cases that would need to be handled. As for the navigation bar, I'm going to have five tabs on it, according to the UI design we came up with. First, of course I need all five buttons to render correctly.

BottomTabNavigator.test.js
test.todo('should have a non null render value')

I then proceeded to the functional tests of my component. I expect to see my screen change when I press the corresponding tab on the navigation bar. So, I added more cases to handle.

BottomTabNavigator.test.js
...
test.todo('should navigate to portfolio screen when portfolio is pressed')
test.todo('should navigate to search screen when search is pressed')
test.todo('should navigate to swap screen when swap is pressed')
test.todo('should navigate to transaction screen when transaction is pressed')

Lastly, I can't forget which screen I'm going to display upon render, and make sure it renders as expected. Also, I need to make sure that the screen doesn't change if I press the currently active tab on the navigation bar. This one was the only negative test I wrote for this component.

BottomTabNavigator.test.js
...
test.todo('should display home screen upon render')
test.todo('should not navigate to another screen when home tab is pressed')

Thus, we have all the business cases.

Write failing tests

After defining all my business cases, I started filling out the todo tests. I first read through the React Native Testing Library (RNTL) documentation to know what I'm supposed to do with the testing. During my exploration, I found some helpful tips.

Take advantage of testID props

The library I'm going to use, React Navigation, provides me a testId props. This is good because then the test will depend neither on text content nor class attributes. So, if someday I need to change those attributes, I will not have to rewrite my tests.

Use getBy instead of findBy

This is another thing I learned (quite) the hard way. I kept getting console warning that looks like this.

console.error
    Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.

      at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
      at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
      at onDone (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15272:9)
      at node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15319:13
      at Immediate._onImmediate (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15244:9)

After some googling, I found that this was caused by my using findBy to test if the element is rendered correctly. The docs said that findBy uses multiple async API calls and that's why I kept getting those console warnings. The warnings disappear after I changed to getBy.

I extracted my component renders to a setup() function and added an afterEach(cleanup) so every test gets a fresh canvas. Also, I wrapped my tests under a single describe() function so that the output is nicer to read.

BottomTabNavigator.test.js
import { render, fireEvent, cleanup } from "@testing-library/react-native";

describe("BottomTabNavigator", () => {
  const setup = () => {
    const component = (
        <NavigationContainer>
          <BottomTabNavigator />
        </NavigationContainer>
    );
    return render(component);
  };

  afterEach(cleanup);

  it("should have a non null render value", () => {
  	const { getByTestId } = setup();

    const homeTab = getByTestId("home-tab");
    expect(homeTab).toBeTruthy();

    // test the rest of the tabs
  });

  it("should display home screen upon render", () => { ... });

  it("should not navigate to another screen when home tab is pressed", () => { ... });

  it("should navigate to portfolio screen when portfolio is pressed", () => { ... });

  it("should navigate to search screen when search is pressed", () => { ... });

  it("should navigate to swap screen when swap is pressed", () => { ... });

  it("should navigate to transaction screen when transaction is pressed", () => { ... });
});

At this point my tests are done. I run it once and makes sure it fails (because why wouldn't it be), and voila. I got a red pipeline.

failing tests
See how the test cases read very nicely?

Write dummy code so the test passes

Next, I need to make the pipeline green. I wrote minimal code to pass the test, and went ahead and push my changes. After tweaking the code a little, I finally got a green pipeline.

At this point, my code is almost a mess. I slapped everything under one file and didn't care as much about clarity. I took care of this on the next step.

passing test
Passing tests are pleasant to see

Refactor & do styling!

This is the fun part. Because I know my component is functionally working, as proven by the passing tests, I know I can style and refactor it however I want as long as I'm still getting that green pipeline. So I went and styled the component according to our UI prototype.

For the refactoring part, I extracted some components and rewrote some parts so that it's a nicer to read. I also deleted some redundant codes, and used array maps wherever I can.

Here's the final component.

final component
The final look

TDD in Action

After some full days of working on the test, the component, and the line coverage, I finally finished this backlog for the sprint. This was one of my successful TDD approach in a while. My fellow El Pepe team members also followed this approach.

pipelines
TDD pipelines in action
pipelines
My code coverage before I submit my MR

TL;DR

TDD is the act of writing tests and implementing features according to said tests. It can aid in business case handling, gives confidence in delivery, and ensures app reliability. There are three steps to doing TDD, but I personally think there are four:

  1. List all business cases related to the feature you're working on
  2. Write failing tests
  3. Write dummy codes to pass the tests
  4. Refactor

That's all for today. See you on the next one~

Footnotes

  1. https://bitbar.com/blog/reaping-the-benefits-of-tdd-for-mobile-app-testing-development/

  2. https://jonathanchristopher1199.medium.com/tdd-perspective-renewed-another-value-c767bdfb9bc5

  3. https://iamshadmirza.com/a-definitive-guide-to-react-native-testing-library

  4. https://www.softwaretestinghelp.com/the-difference-between-unit-integration-and-functional-testing/