This is the second day of my participation in Gwen Challenge

Unit testing is believed to be one of those unused and heard technologies (if you are the big cheese and have heard and used them, your valuable comments are welcome πŸ§Žβ™€οΈπŸ§Žβ™‚οΈ). So what exactly is unit testing, and how can it benefit us in actual project development? Let’s talk about unit testing from a front-end development perspective.

πŸ“š (a) What is unit testing

πŸ“’ Unit test concept

Unit testing refers to checking and verifying the smallest measurable unit in software. In C, a unit is a function; in Java, a class. In graphical software it can refer to a window or a menu. In general, a unit is the module under test that is considered to be the least specified.

This is the introduction of unit testing on Baidu Encyclopedia, so for our front end unit testing is used to test the correctness of a module, a function or a class.

Most unit tests consist of four main bodies:
  • Test suite describe,
  • Test case IT,
  • Judgment condition expect,
  • Asserts the result toEqual.
What is not a unit test

Now that you know what unit testing is, what isn’t it? In the book Modifying code Art, there is an introduction like this:

  • Tests that need to access the database are not unit tests
  • Tests that require access to the network are not unit tests
  • Tests that require access to the file system are not unit tests

So that’s a brief introduction to the concept of unit testing, why unit testing is used, what are the advantages of unit testing, and programmers who don’t think about the payoff are not good programmers.


πŸ“š (2) How does unit testing benefit us in developing programs

  • The first is a fundamental reason for front-end unit testing: JavaScript is a dynamic language, lacks type checking, and cannot locate errors during compilation. JavaScript host compatibility issues. For example, DOM manipulation on different browsers.

  • Correctness: Testing validates the correctness of the code before it goes live.

  • Automation: Of course you can test manually and print out internal information through the console, but this is a one-time thing and the next test will have to be done from scratch, so efficiency is not guaranteed. By writing test cases, you can write them once and run them many times.

  • Explanatory: The importance of test cases to test interfaces, modules, and how to use these apis is covered in test cases. Reading test cases is a good way for other developers to use these apis, sometimes more clearly than the documentation.

  • Drive development and guide design: the premise of the code being tested is the testability of the code itself, so in order to ensure the testability of the code, we need to pay attention to the design of API in the development, and TDD moves the test forward to play such a role.

  • Ensure refactoring: The Internet industry product iteration speed is very fast, there must be a process of code refactoring after iteration, then how to ensure the quality of the refactoring code? With test cases behind you, you can be bold about refactoring.

We know that unit testing with high coverage guarantees a much lower bug rate per launch and is the foundation for code refactoring. In many old projects, the developers quit and the new ones dare not refactor, which gradually becomes a burden on the team and cannot be taken off the line. This is because there is no unit test. Simply put, it can also be summarized as the following points

  1. Improve code quality
  2. Reduce bugs and locate bugs quickly
  3. Feel free to modify and refactor
  4. Unit testing will not only make your job easier. It will make your design better and even reduce the time you spend debugging

πŸ“š (iii) How to write unit test cases

How to write unit test cases and what are the principles of unit test cases:

  • When testing code, consider only tests, not internal implementations;
  • As close to reality as possible,
  • With full consideration of boundary conditions of data
  • Focus, complex, core code, focus tests
  • Use AOP (aspect oriented programming) to reduce test code and avoid useless functionality
  • The combination of testing and functional development facilitates design and code refactoring

πŸ”Ž Insert a small point of knowledge: so what is the meaning of AOP mentioned here, AOP is the acronym of Aspect Oriented Program means Aspect Oriented programming, this kind of programming idea at runtime, dynamically cutting code into the specified method of class, specified location is Aspect Oriented programming.

πŸ“Œ Generally speaking, code snippets that cut into specified classes and methods are called aspects, and which classes and methods to cut into are called pointcuts. With AOP, we can change the behavior of an object by taking the code shared by several classes and pulling it into a slice until we need to cut into it.

After a brief introduction to unit testing, let’s take it into our actual project development and give it a try


πŸ“š (4) after componentization, which part of the component has the most test value? (Use React as an example)

1. Component

Component should focus on render and side effects, while processing of business logic should be extracted as much as possible into Hooks and Utils files. Therefore, for Component testing, we can focus mainly on the following two aspects:

  • Is the component rendering properly?
  • Are component side effects handled properly?
2. Hooks

React-hooks: React-hooks: React-test-renderer: React-hooks: react-test-renderer[2] With these two dependencies, developers can easily mock out the environment on which Hooks execute, treat store data as input for Hooks, focus on the business logic within Hooks, and test Hooks as Pure functions.

3. Redux

For Redux, things are much easier if the project is using the Redux Toolkit and the developer only needs to focus on Actions at Dispatch. However, if Actions and Reducer are written separately, targeted treatment is needed

4. Service

The definition of a Service varies from project to project or team. Here we are talking about the data layer that handles the REQUEST and response of HTTP requests and the corresponding exception handling. The main function of a Service is to interconnect with actions, so ideally the Service only needs to contain the code that communicates with the API, in which case the UT is optional. But some scenario, if the project is not used to assume the role of the data processing BFF, back-end also failed to provide fully meet the demand of the front-end data structure of the interface, inevitably, the developers need to improve the data processing logic here, in order to obtain clean or aggregate data after, so this kind of circumstance, UT is necessary to cover.

5. Utils/Helpers

Helpers include the following types: The conversion of data structures, such as data extraction, merge compression, and collation utility functions common utility functions According to our current project practice, when a piece of logic needs to be implemented in Utils/Helpers, it must be a pure function. Most of these cases involve some degree of data-processing logic, so UT overrides are required

After we know which parts of the project components are most valuable for testing, we are ready to start, and we are ready to try πŸ€Έβ™€οΈπŸ€Έβ™‚οΈ


πŸ“š (5) How to make our test cases easier to write and maintain?

For example, πŸ’πŸŒ°, look at the code first, look not understand it is not important πŸ§Žβ™€οΈ, let’s look at it first

// production code
const computeTotalAmount = (products) = > {
  return products.reduce((total, product) = > total + product.price, 0); 
}

// testing code
it('should return summed up total amount 1000 when there are three products priced 200, 300, 500'.() = > {
  // given - Prepare data
  const products = [
    { name: 'nike'.price: 200 },
    { name: 'adidas'.price: 300 },
    { name: 'lining'.price: 500},]// when - Calls the function under test
  const result = computeTotalAmount(products)

  // then - asserts the result
  expect(result).toBe(1000)})Copy the code

As you can see, we first define a computeTotalAmount function under test, and it wraps our test case. In a test case, the first step is to prepare the data, then call the function under test, and finally print the assertion. You can see that the results are clear and clear. Good unit tests should follow the AAA pattern: Arrange, Act, Assert. This allows you to write clearer test structures that are easy to read and write

The writing unit has the following writing principles:
  • 🎈 Mock data is centrally managed, considering mock data extremes

  • 🎈 focuses only on input and output, not internal implementation

  • 🎈 A unit test tests only one service scenario

    • So that you can give it a good description, the test can protect that particular business scenario, and give you detailed input/output level business feedback when it dies.
  • 🎈 is very expressive and does not contain logic

    • Expressive test, which can give you feedback very quickly when failure, see the test, you will know that it is measuring point of the business what test hang up, can clearly know the business scenario, the differences between the expected data and the actual output, like write declarative code, test need is a simple statement: Prepare the data, call the functions, assert, and make it clear at a glance what the test is measuring. If there is logic in it, take time to understand it; Once the test fails, how do you know if the implementation is dead or the test itself is dead?
  • 🎈 runs fast

    • Mocks can be used to properly isolate dependencies from three parties, strategically placing dependencies, integrations, and other time-consuming dependencies that are returned by three parties into higher-level testing
  • 🎈 isolation,

    • Unit testing is the testing code independent unit, the meaning of the independence does not mean 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

Finally, let’s take it into the project and actually develop it


πŸ“š (6) React unit test framework enzyme application

1. Test tools: The main test tools used are Jest and enzyme
2. Components to be tested: a simple list that can be added and deleted;
3. We want to test four points:

1. Component rendering

2. Initial to-do list display during rendering

3. We can create a new to-do list and return three todos

4. We can delete an initial to-do list and keep only one

Ing on the code to…

The component πŸ‘© 🌾 πŸ‘© 🌾 πŸ‘© 🌾

import React, { useState, useRef } from "react";
const Todo = () = > {
    const [list, setList] = useState([
        { id: 1.item: "Fix bugs" },
        { id: 2.item: "Take out the trash"}]);const todoRef = useRef();
    const removeTodo = id= > {
        setList(list.filter(todo= >todo.id ! == id)); };const addList = data= > {
        let id = list.length + 1;
        setList([
            ...list,
            {
                id,
                item: data
            }
        ]);
    };
    const handleNewTodo = e= > {
        e.preventDefault();
        const item = todoRef.current;
        addList(item.value);
        item.value = "";
    };
    return (
        <div className="container">
            <div className="row">
                <div className="col-md-6">
                    <h2>Add Todo</h2>
                </div>
            </div>
            <form>
                <div className="row">
                    <div className="col-md-6">
                        <input
                            type="text"
                            autoFocus
                            ref={todoRef}
                            placeholder="Enter a task"
                            className="form-control"
                            data-testid="input"
                        />
                    </div>
                </div>
                <div className="row">
                    <div className="col-md-6">
                        <button
                            type="submit"
                            onClick={handleNewTodo}
                            className="btn btn-primary"
                        >
                        Add Task
                        </button>
                    </div>
                </div>
            </form>
            <div className="row todo-list">
                <div className="col-md-6">
                    <h3>Lists</h3>{! list.length ? (<div className="no-task">No task!</div>
                    ) : (
                        <ul data-testid="list">
                            {list.map(todo => {
                                return (
                                    <li key={todo.id}>
                                        <div>
                                            <span>{todo.item}</span>
                                            <button
                                                className="btn btn-danger"
                                                data-testid="delete-button"
                                                onClick={()= >RemoveTodo (todo. Id)} > delete</button>
                                        </div>
                                    </li>
                                );
                            })}
                        </ul>
                    )}
                </div>
            </div>
        </div>
    );
};
export default Todo;
Copy the code

The components are simple, simple functions to add and remove, but looking directly at the code can feel messy and not at all clear, so let’s look at the test case, Ula……

import React from "react";
import { shallow, mount } from "enzyme";
//import 'jsdom-global/register'; // Open when testing the completion of a single test
import App from "./App";

describe("Todo".() = > {
  // Render a component as a virtual DOM object, but only the first layer is rendered, not all the child components, so processing is very fast
    it("Component Render".() = > {
        shallow(<App />);
    }) 
    it("Query li number".() = > {
      const wrapper = mount(<App />);
      // Mount can render all child components under component APP
      expect(wrapper.find("li")).toHaveLength(2);
      // We find element li. Since there are two default values for list, there should be two for element li.
    });
    it("Call addList".() = > {
      const wrapper = mount(<App />);
      wrapper.find("input").instance().value = "Add a new one";
      // Find the input element in the same way and assign it the value "add one"
      expect(wrapper.find("input").instance().value).toEqual("Add a new one");
      // After the assignment is complete, we find the corresponding input element, query the value of the input, and add assertions to determine whether it is "new".
      wrapper.find('[type="submit"]').simulate("click");
      // Find the button whose type is submit and trigger the click event
      expect(wrapper.find("li")).toHaveLength(3);
      // Assert whether the length of li is 3
      expect(wrapper.find("li div span").last().text()).toEqual("Add a new one");
      // Assert the last li(the newly added one), and whether the child element span is "newly added"
    });

    it("Call removeTodo".() = > {
      const wrapper = mount(<App />);
      wrapper.find("li button").first().simulate('click');
      // Find a button in li that triggers the click event
      expect(wrapper.find("li")).toHaveLength(1);
      // If the delete button is triggered, the length of the query li should be 1
      expect(wrapper.find("li div span").last().text()).toEqual("Take out the trash");
      // Check whether the value of span is Take out the trash})});Copy the code

So let’s run yarn test

You can see the run time for each test case

Let’s try jest –coverage again

You can see that our lines coverage is 100%

So that’s a brief introduction to unit testing, with the final example 🌰 πŸ“Žgithub address attached