For today’s front-end snack, what should unit tests measure in a project?

At the pace of domestic Internet development, full coverage of unit testing in front end business projects is sometimes not feasible, mainly due to the following stumbling blocks:

  • UI interactions are complex and paths are difficult to cover comprehensively
  • Deadlines are tight, and developers are not confident in the long-term benefits of TDD and BDD practices
  • From time to time, product managers change requirements in the name of “agile development,” making the test scripts they’ve just laboriously written completely useless

In such a situation, it doesn’t make much sense to emphasize the logical coverage of unit tests, and it makes more sense to figure out where single tests can get the most marginal benefit.

Here are three ideas about what unit testing should measure, based on my experience in single testing, with some examples:

Unit testing is not all there is to testing

Think of unit tests in a philosophical way

Recognizing that single test is only a partial module test, one of many test scenarios, can avoid testing for testing’s sake, or testing for metrics’ sake.

At the same time, it should also be realized that the coverage ability of single test itself is also limited. The PASS of all use cases and 100% coverage cannot guarantee the correct behavior of all logical paths of the tested module.

Whether to use unit tests on a module often depends on the logical stability and type of business of the module

For example, for an underlying NPM package project, unit testing is almost the only means of code quality assurance. At this time, unit testing should be used to verify whether its behavior in various application scenarios meets expectations, so as to ensure the quality of each package and update at the lowest cost. For such projects, a thorough application of THE BDD development model will also yield increasing efficiency benefits.

For a UI component with complex functions, in addition to unit testing, there are E2E testing, automated regression testing, and QA manual testing (😊) to ensure its code quality. At this point, the marginal benefit of using unit testing may not be the highest, so you can consider other means to regression its logic. You can also consider using snapshot tests to regressively verify the logic of each iteration after the initial validation goes live.

Simulation of boundary environment

Let the module travel through time

One of the most important aspects of single testing is that it helps us simulate boundary scenarios that are hard to reach in QA manual testing (😊) and even in online use scenarios, such as:

  • Emulates JS versions in individual browsers
  • Simulates a URL state
  • Simulate some local cache state
  • Simulate different time zones
  • The simulation time passed an hour (which almost only unit tests can do)

, etc.

The marginal benefit of unit testing modules using this type of simulation is extremely high and often much faster than QA doing equivalent simulations.

For example, here is a script that uses jest’s Timer mock capability to test the EXPIRE function:

const expire = (callback) = > setTimeout(callback, 60000); // It will expire in one minute

test('Call the callback when it hits the dot', () = > {const callback = jest.fn();
  expire(callback);

  jest.advanceTimersByTime(59999);
  expect(callback).not.toBeCalled();

  jest.advanceTimersByTime(1);
  expect(callback).toBeCalledOnce();
})
Copy the code

This code accurately simulates the execution of the macro task through jest. AdvanceTimersByTime, synchronizing the testing of an asynchronous process that would otherwise take a minute to validate once.

For example, the following test script is used to test a utility function called catchFromURL that takes the specified parameter from the current URL and returns it as a return value while erasing it from the URL.

This is very common in business scenarios (such as single sign-on) that require token information to be carried through urls.

test('Get the specified parameter value from the URL and erase it', () = > {const CURRENT_ORIGIN = document.location.origin;
  const testHref = `${CURRENT_ORIGIN}/list/2/detail? a=123b&b=true#section2`;
  history.replaceState(null.' ', testHref);
  expect(catchFromURL('a')).toBe('123b');
  expect(document.location.href).toBe(`${CURRENT_ORIGIN}/list/2/detail? b=true#section2`);
})
Copy the code

This test code uses JsDOM to simulate the environment to be tested. Environment construction and simulation is actually a difficult point in unit testing. Due to some defects of JsDOM itself (such as not implementing Navigator), it often needs a lot of Hack techniques to simulate the correct browser environment in the Node environment where the test script is running. This point will be discussed in the future night point mind.

nudges

less is more

The test code doesn’t need to be concerned with the specific implementation of the module being tested, just test a few necessary process scenarios as far as possible. This reduces the time it takes to write the test logic and gives the business logic more freedom to implement.

For a business module, the test script only needs to care about all the externalities associated with the module:

  • For a functional module, control the module it references, its inputs, and its side effects, and verify its output and impact on side effects
  • For a component module, it controls the services it depends on, the subcomponents it depends on, its props, and its events, and verifies its render results and calls to callbacks in its props, regardless of its state.

The following script tests a React component named ValidatableInput using the Enzyme component test tool. This component triggers the onValidate callback when it is out of focus and passes in the inputValue parameter.

  test('Out-of-focus trigger onValidate', () = > {const onValidate = jest.mock();
    const inputValue = '输入的内容';
    const wrapper = shallow(
      <ValidatableInput
        placeholder={"'}value={inputValue}
        alert={"'}onChange={onChange}
        onValidate={onValidate}
      />
    );

    wrapper.find('.validatable-input').first().simulate('blur');
    expect(onValidate).toBeCalledWith(inputValue);
  });
Copy the code

In the above test case our test logic is based entirely on behavior, focusing only on the out-of-focus “action” and the “feedback” of performing callbacks, without asserting anything about the state of the component.

This gives the component the freedom to implement its internal logic as it sees fit, such as adding the ability to provide value and onChange as controlled components through external providers. None of these implementation changes will affect the validity of the current test case.


Here are some ideas on what unit testing should do. Single testing does what it does best to get more bang for its buck in a tight development pace.

In the following extended reading, an article on whether test coverage is a true engineering quality indicator is posted for those who are interested

Github original link

Further reading

  • Is unit test coverage a real quality metric?