Hackernoon logoHow React Testing Library Can Improve Your Mental Health [Part 1] by@mac

How React Testing Library Can Improve Your Mental Health [Part 1]

Mac Wasilewski Hacker Noon profile picture

@macMac Wasilewski

During the day a developer. A husband and dad at night.

I know one person who likes writing tests, and it is not me! I like adding code-coverage to my code and you'll see why.

My name is Maciej, I'm a software engineer, and you can find how to pronounce my name here: sayoname.com/me/maciej

Omitting tests is OK only if the project is your small to-do app. 

Not having tests will become a problem sooner or later. Now you might deploy features quickly. But the day when you cannot release for weeks because of cropping bugs here and there will come and this will happen...

If the project grows it WILL become a problem! Imagine - you start getting users, making money, hiring devs, there are so many feature going on that you will not be able to keep all those dependencies in your head.

I worked in a place once which had a pretty big code project with no tests at all. The repo was massive, thousands of customers used it every day and they paid millions. But customers were paying and they wanted new features. Some were simple, like changing text and adding a button here and there. Some were more complex like providing integration with external software.

One day we got our Product Owner (PO) who specified this new requirement. We needed a few new reports for customers, which were basically a couple of new tables. What started as a small task which we scoped around 4-weeks until production. Turned out to be a 4-month struggle with tens if not hundreds of bugs raised by QA.

Why was that? It wasn’t the technology used, it wasn’t the developers ;) and it wasn’t the complexity of the project. It was the lack of tests!

There were three of us working on those features, and due to no tests, we didn't know if our changes break someone else's code!

Another problem was that those bugs were usually found after a couple of days of full manual regression testing. After a bug was raised we had to pick bug fixes as a priority and switch context. And then investigate, trying to remember why I wrote this rubbish code, fix it, create a PR, get it approved, and then release (obviously manually, because what did you expect from a no tests codebase).

This massive gig made the business realize we cannot do it anymore and a decision has been made to rewrite the whole app! It took almost 18 months and obviously, no new features were added in the meantime.

You get now why tests are important. You don't? Close the page, you'll never get it! Get back to adding functionality and stop wasting your precious time even reading about tests!

Still here? Let's start writing tests!

So to prepare you to add and write tests we are going to work on this small create-react-app repo where I added some Redux.

Our plan is the following:

  1. setup test coverage
  2. start with simple redux testing
  3. let's test a react component
  4. snapshots and why they are good
  5. why to never rely on snapshots
  6. let's spy and mock-implement a function
  7. spy-on a module!
  8. let’s test some async calls made by axios
  9. cookies and globals

Adding test Coverage

Why test coverage? If you write tests you want to know what parts of the apps are tested, don’t you?

Generally, test coverage will tell if you are testing all lines of code, all if-else statements. You want 100% code coverage. If someone says that you don’t, he is lying to you. There are some cases when it is hard to reach it but always aim for 100%.

So how to add it in?

First, open package.json and add the below code:

    "jest": {
        "coverageThreshold": {
            "global": {
                "branches": 100,
                "functions": 100,
                "lines": 100,
                "statements": 100
            }
        }
    },

What the above means is that we require the code to have 100% code coverage. We want all the branches, all the functions, absolutely every single line, and statement. All of it!

Then we enforce it on all those poor colleagues who don't expect the worst yet :)

Now install husky by running:

npm i --save-dev husky

Now we can add a pre-commit hook which will prevent anybody from successfully committing and pushing code that doesn’t adhere to our high standards. (Pro tip: there is a way to omit it but if you do it I'll tell your mom!)

Now after installing husky add the below to package.json so we'll run all tests before every commit.

"husky": {
    "hooks": {
      "pre-commit": "npm run test:watch",
    }
  }

And finally, update the scripts object in package.json with

"test": "jest --coverage",
"test:watch": "npm test -- --watchAll"

Now if you run

npm run test:watch
an additional folder Coverage will appear in your root directory. It contains ‘lcov-report’ and this is where index.html sits. If you open it in the browser it won’t contain anything as we haven’t got any tests yet! So let's add some tests!

Start with simple redux testing

The reducer.js code which we are testing;

const initialState = {
    todos: [
        { id: 1, text: 'Do boring stuff', completed: false, color: 'purple' },
        { id: 2, text: 'Write tests!', completed: false, color: 'blue' }
    ],
    filters: {
        status: 'All',
        colors: []
    }
}

function nextTodoId(todos) {
    const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
    return maxId + 1
}

export default function appReducer(state = initialState, action) {
    switch (action.type) {
        case 'todos/todoAdded':
            {
                return {
                    ...state,
                    todos: [
                        ...state.todos,
                        {
                            id: nextTodoId(state.todos),
                            text: action.payload,
                            completed: false
                        }
                    ]
                }
            }
        case 'todos/todoToggled':
            {
                return {
                    ...state,
                    todos: state.todos.map(todo => {
                        if (todo.id !== action.payload) {
                            return todo
                        }

                        return {
                            ...todo,
                            completed: !todo.completed
                        }
                    })
                }
            }
        case 'filters/statusFilterChanged':
            {
                return {
                    // Copy the whole state
                    ...state,
                    // Overwrite the filters value
                    filters: {
                        // copy the other filter fields
                        ...state.filters,
                        // And replace the status field with the new value
                        status: action.payload
                    }
                }
            }
        default:
            return state
    }
}

Go and create a new reducer.test.js file.

Now import the reducer function

import appReducer from './reducer';

I like to write my test by starting with a `describe` block which helps me split my tests. See below:

describe('appReducer', () => {
 
  
})

As we are testing a function called appReducer the describe should contain the name of the function. This way if you've got a failing test, and you will get to tens of tests in a single file it is easier to find it, as the failed test will be nested under the describe string, in our case appReducer.

Let’s start with the simple default case which just returns the state which was passed to appReducer.js. As we see in the code the appReducer.js accepts two arguments, state, and action. If we pass an empty object as initial state and an action with empty type. We expect that function to return the empty object. See below:

describe('appReducer', () => {
   it('should return the initialState when no action has been used', () => {
    expect(appReducer({}, {type: ''})).toEqual({});
  }) 
})

As you can see we add our test by creating an it() function that accepts a string, and a callback function as arguments. The string is used to tell the ‘future’ yourself what is being tested and the callback is just the body of the test, where we run our tested function and then assert results.

Now run `npm run test:watch` and it is green! Hooray! 

Now go to coverage/lcov-report/index.html and see in the browser what code is covered. A red (Oops) link called reducer.js (name of the file which we are testing.

Click on the link and you’ll see the coverage, like below:

The yellow lines mean that our tests do not pass the condition required to enter the body of the statement. So the condition has been hit but we need to add a test to cover that specific case. In the end, you see the default case which has full coverage. Is not red or yellow, has a 1x on the left meaning that our tests hit it once. What makes sense as we have a single tests which runs appReducer once.

We are aiming for 100% so let’s add more tests. But all that and the react-testing-library in the next part :)

Part two where we actually start using react testing library : How React Testing Library Can Improve Your Mental Health [Part 2] | Hacker Noon

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.