React Hook: Why Does React need Hook? React Hook: Why does React need Hook? React Hook: Why does React need Hook? In this article I’ll walk you through how to improve the quality of our code by writing unit tests for custom hooks, which include the following:

  • What are unit tests
    • Definition of unit tests
    • Why write unit tests
    • What should unit tests look for
  • How to unit test custom hooks
    • Jest
    • React-hooks-testing-library
    • example

What are unit tests

Definition of unit tests

To understand unit testing, let’s define testing. In the simplest terms, testing is: We give the tested object some inputs and see if the output matches with expected result. There are many different types of tests in software engineering, such as unit test, functional test, performance test and integration test. The main difference between different types of tests is that the subjects and indicators are different. Unit tests are individual units of our source code, and in procedural programming, units are functions that we encapsulate, In object-oriented programming, a unit is a method of a class. It is generally not recommended to use a class or module as the unit of a unit test, because this will make the logic to be tested too large. And problems are not easy to locate when they occur.

Why write unit tests

Now that we know what unit tests are, let’s look at why we do unit tests in our code.

The main reason for writing unit tests in a project is that unit testing your code has the following benefits:

Improve code quality

Unit testing improves the quality of our code primarily by helping us find bugs in our code before we develop a feature. For example, if student A writes A hook called useOptions, it takes A parameter called options, which can be either an object or an array. In the process of his own development, student A only tried to pass objects to useOptions but not arrays to it. B classmate of the same project in using useOptions pass an array to it found hanging code, this time B students have to find A classmate to confirm and wait for A classmate to repair this problem, this not only affects the development progress of class B and B students will feel A classmate, or find A classmate of the code is bad. If student A had unit tested useOptions, this tragedy might not have happened, because student A considered the situation that Options was an array when writing the unit test for useOptions, and fixed the problem before student B used it. Thus, writing unit tests allows us to improve the quality of our code by thinking ahead to problems that will be discovered later.

Easy to refactor code and add new features

The process of writing unit tests is really the process of writing instructions for our code. This instruction manual is very important. It is like a contract between the producer and the consumer. The producer needs to ensure that the code has the effects described in the instruction on the premise that the consumers use the code correctly. This actually puts constraints on the code producer, who must ensure that the original code meets the requirements specified in the original instruction manual, whether adding new features or refactoring it.

Continuing with the above example, both STUDENT A and student B used useOptions hook in the 1.0.0 version of the project. Although useOptions did not write unit tests, the code was bug-free (at least not found). Later, the project needs to be upgraded to version 2.0.0. At this time, student A needs to add new functions for useOptions. After changing the code of useOptions, student A tested it in the place where she used it (object as parameter) and found no bugs. After student A tested the code by himself, he integrated the change into the master branch of the project. Later, after updating the code of STUDENT A, student B found some problems in his own code. At this time, student B may be in A mess, and it may take some time to locate the original change of useOptions by student A that affected his function. This will not only affect the progress of the project but also worsen the relationship between STUDENT A and student B. This tragedy can also be avoided by writing unit tests. For example, if student A writes A matching instruction manual (unit test) for useOptions, after changing the code, its code cannot pass the inspection of the instruction manual. Because its modification has changed the external behavior defined by useOptions before, student A will repair his code in advance to avoid the trouble of student B. Through this example we probably didn’t realize the unit tests for our product at ordinary times the importance of the iteration or code refactoring, but do you imagine in A larger project that has many A students and B students is, there are tens of thousands of useOptions function, serious happen similar problems bugs will be harder to locate and repair, If most of our code had unit tests, we would have more confidence in adding new features to the code and refactoring the original code.

Improve the design of our code

There is a concept in software engineering called Test-driven Development that encourages us to write Test cases for our code before we actually start coding. The goal is for us to judge our code design from the perspective of the code user before development. If our code design is bad, we will find it difficult for us to write the detailed unit test cases, on the contrary if we code design is very good (low coupling and high cohesion), every function parameters and function of the design is reasonable, we are very easy to write the corresponding unit test for them. We can bear in mind that high-quality code can be tested. So why write test cases before you even start writing code? This is because if we then write the code written test, even if we find that the code design not reasonable, we have no incentive to change, because the may make changes to the design we rewrite all the code, so we need to write unit test before the actual coding, because this time the change of code is the smallest resistance.

Provide documentation function

When we write unit tests for our code, we are actually writing use cases for our code, so that other developers can use our unit tests to quickly learn how to use the various functions we define. Another useful tip: if we find that the documentation of a library is not comprehensive, we can quickly learn how to use the library by looking at its unit tests.

Unit testing issues to watch out for

Isolation,

Above we talk about unit testing is testing code independent unit, the meaning of the independence is not to say that this function (unit) will not call another function (unit), but we have a test this function if it calls to other functions we need to mock them, so will we test logic only put on by the logic of test functions, Not affected by other dependent functions. For example, we will now test the following function:

async function fetchUserDetails(userId) {
  const userDetail = await fetch(`https://myserver.com/users/${userId}`)
  return userDetail
}
Copy the code

We need the mock Fetch function when we test fetchUserDetails, because the function we’re testing now is fetchUserDetails, and we just need to make sure that the fetch will be called when fetchUserDetails is called from outside, {username} and the parameters of the call is the “https://myserver.com/users/$”, as to how the fetch function request and deal with the data is returned to the fetch function to do your own thing, We should not worry about this when testing fetchUserDetails.

Another reason for unit testing to care about isolation is that it makes it easy to locate the problem when a test case fails. For example, if we didn’t have the mock Fetch function, once our test failed, it would be hard to tell whether the fetchUserDetails logic was wrong or the FETCH logic was wrong.

repeatability

All the unit test cases we write must not depend on the external running environment, otherwise our unit tests will not be repeatable. Repeatability is this: if our unit test case passes now, it will always pass as long as the code doesn’t change and the test case doesn’t change. As an example of test cases do not have repeatability, if you put all project unit test data in the database, you run the project today test cases can be passed, and the next day everyone else has no intention to change the database data, your test cases by this time, we say these test cases do not have repeatability, The main reason for this problem is that they use external dependencies as test conditions. Thus, one of the key points to make our test cases repeatable is to avoid external dependencies when writing unit tests, such as databases, network requests, and local file systems.

Another important but overlooked factor affecting test case repeatability is that some test data is shared between different unit test cases, and changes made to test data by one test case may affect the correct execution of other test cases. Therefore, when writing unit test cases, we must avoid sharing some test data between different test cases and try to isolate each test case.

Improve code coverage

There’s a concept in unit testing called test coverage, which shows how much of our code is being tested. For example, if we have a 100-line function, and after we run all the unit test cases written for that function, if the test framework tells us that the function has 80% coverage, that means that our test case code only covers 80 lines of the function, There are also some code branches (if/else, switch, while) that are not executed. If we want to pass the unit test to improve the quality of our code, we need to ensure that our code coverage is enough big, try to get the tested function of each kind of be implementation is covered (100% coverage), in particular, some abnormal situation should also be covered (parameter errors, for example, called the third party relied on an error, etc.), This way we can find bugs in the code early and fix them.

Test case run times should be short

I mentioned above that unit testing can help us better iterate and refactor code. This actually requires us to perform some automatic detection (CI) of the merged code every time the code merges. This includes running the project unit test cases. Imagine that in a relatively large project, the number of unit test cases is often very large, sometimes hundreds, sometimes thousands. If it takes ten minutes or even one or two hours to run all the test cases, it will affect the progress of code integration. To avoid this problem, we need to ensure that each unit test case does not take too long to execute, such as avoiding time-consuming calculations in the test code.

How to unit test custom hooks

In the React Hook Field guide, we mentioned that hooks are functions, so a unit test of a Hook is actually a test of a function. The difference between this function and a normal function is that it has special functions assigned to it by React. Jest and React-hook-testing-library are used to test hooks. Jest and React-hook-testing-library are used to test hooks.

Jest

Jest is an open source unit testing framework for Facebook. It is very popular and well-known. Some well-known open source projects such as WebPack, Babel and React use Jest for unit testing. So I’m not going to give you a detailed introduction here, but I’m going to introduce the Jest API that we commonly use:

Commonly used API

it/test

The it/test function is used to define test cases. Its function signature is IT (description, fn? , timeout?) The description argument is a short description of the test case, fn is a function that runs our actual test logic, and timeout is the timeout of the test case. Here’s a simple example:

import sum from 'somewhere/sum'

it('test if sum work for positive numbers', () = > {const result = sum(1.2)
  expect(result).toEqual(3)})Copy the code
describe

The describe function is used to group test cases. Its function signature is describe(description, FN), and description is used to describe the group. The FN function can define nested groups or test cases. Here’s a simple example:

import sum from 'somewhere/sum'

describe('test sum', () => {
  it('work for positive numbers', () = > {const result = sum(1.2)
    expect(result).toEqual(3)
  })

  it('work for negative numbers', () = > {const result = sum(- 1.2 -)
    expect(result).toEqual(- 3)})})Copy the code
expect

As we mentioned at the beginning, testing is the process of comparing the output of the object being tested to the expected output. In the Jest framework, we can use the Expect function to access a series of matchers to do this process. For example, expect(sum).toequal (3) above is a process that uses matcher to determine whether the output is the desired value. For more information on matcher, see jEST’s official documentation.

mock

There are a number of methods used to mock within the Jest framework, the main ones being Jest. Fn () and Jest. SpyOn ().

jest.fn

Jest. Fn generates a mock function that can be used to replace third-party functions used in the source code. Jest. Fn generates a function that has a number of attributes, and we can also use some matcher to assert this function. Here is a simple example:

// somewhere/functionWithCallback.js
export const functionWithCallback = (callback) = > {
  callback(1.2.3)}// somewhere/functionWithCallback.spec.js
import { functionWithCallback } from 'somewhere/functionWithCallback'

describe('Test functionWithCallback', () => {
  it('if callback is invoked', () = > {const callback = jest.fn()
    functionWithCallback(callback)

    expect(callback.mock.calls.length).toEqual(1)})})Copy the code
jest.spyOn

The function in our source code may use another file or some dependencies installed in node_modules, which can be mock with jest. SpyOn. Here’s a simple example:

// somewhere/sum.js
import { validateNumber } from 'somewhere/validates'

export default (n1, n2) => {
  validateNumber(n1)
  validateNumber(n2)

  return n1 + n2
}

// somewhere/sum.spec.js
import sum from 'somewhere/sum'
import * as validates from 'somewhere/validates'

it('work for positive numbers', () = > {// mock validateNumber
  const validateNumberMock = jest.spyOn(validates, 'validateNumber')
  
  const result = sum(1.2)
  expect(result).toEqual(3)

  // restore original implementation
  validateNumberMock.mockRestore()
})
Copy the code

When we introduced the source code dependency Somewhere/Validates in the test code above, we could mock out the export-dependent methods with jest. SpyOn, for example, validateNumber. The mock function will be used when the source code is executed. For example, the sum execution validateNumber is the validateNumberMock defined in sum.spec.js. We can assertion sum against validateNumberMock to ensure that the validateNumber doesn’t interfere with the sum function. It is also important to note that I called the mockRestore function after the test case execution, which restores the original implementation of the validateNumber function to prevent this test case’s changes to the validate file from affecting the correct execution of the other test cases.

The project introduces JEST

Now that we’ve looked at some of jEST’s basic apis, let’s look at how to introduce JEST into our project.

Install dependencies

First install jest using the following command

yarn add -D jest
Copy the code

If your project uses Typescript, you also need to install ts-Jest as a dependency:

yarn add -D ts-jest
Copy the code

Configuration jest

After installing Jest, you need to configure it in the package.json file:

{ 
  "jest": {
    "transform": {
      "^.+\\.tsx? $": "ts-jest"
    },
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx? |tsx?) $"."moduleDirectories": [
      "node_modules"."src"]."moduleFileExtensions": [
      "ts"."tsx"."js"."jsx"."json"."node"]}}Copy the code

The meanings of the above configuration items are as follows:

  • Transform: Tell Jest that your TS or TSX files need to be converted using TS-Jest.
  • TestRegex: tells Jest which files need to be executed as test code. From the regular expression above, we can see that files with test and spec names will be executed as test cases.
  • ModuleDirectories: Tells Jest which directories to resolve when executing the test case code for dependencies that the code uses, where Jest will gonode_modulesandsrcResolve (or your own source code root) should be consistent with the resolve section of your project’s webpack.config.js configuration.
  • ModuleFileExtensions: Tells Jest which file suffixes to try if the corresponding file cannot be found.

React hooks testing library

React-react-testing-library is a library designed to test React Hooks. We know that a hook is a function, but we can’t test it in the same way that we would test a normal function. Because running them involves a lot of React runtime stuff, many people write testComponents to run them to test their hooks. This approach is inconvenient and difficult to cover all scenarios. To simplify the process of testing hooks for developers, the React community developed a library called react-rex-testing-library that allows us to test our defined hooks as if they were normal functions. The library actually runs our defined hooks inside a TestComponent, but it encapsulates some simple apis to simplify testing. Before we start using the library, let’s take a look at some of the common apis it exposes.

Commonly used API

renderHook

The renderHook function is used to render the hook as the name implies. It will render a TestComponent that uses our hook when called. RenderHook’s function signature is renderHook(callback, options?) The first argument is a callback function that will be called every time TestComponent is re-rendered, so we can call the hook we want to test in this function. The second argument to the renderHook is an optional options. This option can take two properties, initialProps, which is the initialProps parameter of TestComponent and is passed to the callback function to call the hook. The other property of Options is wrapper, which specifies the wrapper Component parent of TestComponent. This component can be something like a ContextProvider that provides test data to the TestComponent hooks.

RenderHook returns a RenderHookResult object, which has the following properties:

  • The result:resultIs an object containing two properties, one of which iscurrentWhat it preserves isrenderHook callbackThe other property iserrorThis is used to store any errors that occur in the hook during render.
  • rerender: rerenderThe function is used to re-renderTestComponentIt can accept a newProps parameter, which will be used as the props value when the component is re-rendered, againrenderHookthecallbackThe function will also be called again using the new props.
  • unmount: unmountThe function is used to unloadTestComponentIt is mainly used to cover someuseEffect cleanupFunction scenario.
act

This function is the same as the React act function. When a component’s state is updated (setState), the component needs to be re-rendered. This re-rendering is scheduled by React, so it is an asynchronous process. We can use the ACT function to encapsulate all operations that update to the component state in its callback to ensure that the component has been rerendered after the act function is executed.

The installation

Add react-links-testing-library to your project devDependencies:

yarn add -D @testing-library/react-hooks
Copy the code

Note: To use the react-react-testing-library we need to make sure we have react and react-test-renderer installed on version 16.9.0 and above:

Yarn add react@^16.9.0 yarn add -d react-test-renderer@^16.9.0 yarn add -d react-test-renderer@^16.9.0Copy the code

example

Let’s look at a simple example of using Jest and react-react-testing-library to test a hook. Suppose we define a hook called useCounter in our project:

// somewhere/useCounter.js
import { useState, useCallback } from 'react'

function useCounter() {
  const [count, setCount] = useState(0)

  const increment = useCallback((a)= > setCount(x= > x + 1), [])
  const decrement = useCallback((a)= > setCount(x= > x - 1), [])

  return {count, increment, decrease}
}
Copy the code

In the code above, I defined a hook called useCounter that encapsulates a state called count and exposes a number of updaters that operate on count, including increment and Decrement. If you’re not familiar with useState and useCallback, check out my last post on React Hook. Let’s write a test case for this hook:

// somewhere/useCounter.spec.js
import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from 'somewhere/useCounter'

describe('Test useCounter', () => {
  describe('increment', () => {
     it('increase counter by 1', () = > {const { result } = renderHook((a)= > useCounter())

      act((a)= > {
        result.current.increment()
      })

      expect(result.current.count).toBe(1)
    })
  })

  describe('decrement', () => {
    it('decrease counter by 1', () = > {const { result } = renderHook((a)= > useCounter())

      act((a)= > {
        result.current.decrement()
      })

      expect(result.current.count).toBe(- 1)})})})Copy the code

In the code above, we wrote a describe Test useCounter and defined two Test groups to Test the increment and Decrement methods returned by useCounter. Increase counter by 1: increase counter by 1: Increase counter by 1: Increase Counter by 1: Increase Counter by 1: Increase Counter by 1 This is because we need to retrieve the return result of this hook outside {count, increment, decrement}. Then we use act function to call increment function to change component state count. After act function is completed, our component has completed re-rendering, and then we can determine whether the updated count is the result we want.

conclusion

In this article I show you what unit testing is, why we need it in our own projects, and how to use Jest and the React-react-testing-library to test our own custom hooks.

This is the last article in my React Hook series, and I will continue to share some hook related articles. Stay tuned. If you feel helpful to you, welcome to like and follow!

reference

  • jestjs.io/
  • react-hooks-testing-library.com/

Personal Technology dynamics

The article started on my personal blog

Welcome to pay attention to the public number of green Onions to learn and grow together