1. Background

The merchant system is a service tool for Dewu merchants to operate stably on the Dewu platform, and the front-end code is constantly evolving along with the development of the system.grow.This willThe document is not updated in timeFinally, it is very difficult to trace back the business logic through these documents.

And if there is no attention to the code structure, a file with a size of several thousand lines will be produced at every turn. It is difficult to sort out the logic inside when personnel alternate maintenance, and maintenance is very difficult.

2. Difficulties in front-end unit testing

In order to solve the above pain points, the team has done some other things to make the documentation clearer and the code quality higher before the single test, such as writing requirements system documentation, passingThe clean architecture Layer code, code review, and more. But these are actually only external constraints. Only when the internal code can truly withstand the scrutiny of the unit test can we better guarantee the quality of our code.

But the current status is that the front-end has not been exposed to unit testing in most cases, only some in component libraries or tool projects. This does not mean that the front-end in business projects cannot be single-tested, but because of some objective reasons, the front-end invests relatively little in single-testing.

  1. The content of front-end development is relatively complicated. A requirement is not only the writing of functional functions, but also the display of UI, the binding of dom interaction, etc., and if you want to fully cover the single test, it will contain a lot of content. For the business front-end Say the cost is too high.

  2. Front-end UI frameworks emerge in endlessly. During business development, it is easy to completely couple the code logic and UI together by relying on the framework, resulting in thousands of lines in a file. It is difficult to find an entry point for single testing of this code.

  3. There is a certain threshold for getting started with a single test. It is not easy to write a single test with high maintainability, which will discourage people who are not familiar with it.

3. Single test is document

In view of the first difficulty above, the content involved in the front end is too complicated, and we certainly cannot cover all the codes with single tests to test every corner of the code.Combined with our own pain points (Documents are not updated in time, and the cost of personnel rotation is high)so with the goal of “single test is document”, we only need to cover the single test of business logic, only focus on the connection of business process, explain the business process clearly through use cases, and do not do any branch coverage of single test Strong demands.

Use Cases

Therefore, the first step to implement a single test in the team is toIdentify the code modules that implement the business logic. If it was earlier, there might not be a good way to find this entry point, because they are all large files with thousands of lines, and the logic and UI are coupled together.

As mentioned earlier, we have done some code preparation work before the single test implementation.benefited from “Clean Architecture” With the implementation of the development requirements, the code has been gradually decoupled and refactored. The core is to split it into different levels according to the different functions of each part of the code, and to formulate a clear definition between each level.Dependency Principle,It has nothing to do with the framework, has nothing to do with external services, and can be tested.

After layering, we mainly put business logic inuse caseThis layer, in our code structure, its function is to connect business processes in series, and it only depends onentities(Mainly adapt and check the data returned by the server) layer, logic independence will not fail to run due to changes in the framework or UI.

Compared with back-end services, front-end applications usually do not carry real business logic such as computing and storage. At the same time, due to the popularity of micro-service architecture, front-end applications often bear heavy glue logic, that is, each micro-service The logic is connected in series to run through the business process.

Therefore, when the front end writes usecase,We will pay more attention to the splitting of main and sub-functions, so that the main usecase can describe the business process more purely. Part of the specific implementation is split into sub-functions for implementation.

/*
    usecase聚焦流程的描述,诸如url链接拼接、活动期查询等具体逻辑都拆分到了其他的模块中
*/
async function exportActivityLog({count, formValues}: {count: number;formValues: LogData}) {
  if (count > 5000) {
    message.error('导出文件数量不得超过5000!')
    return
  }
  const res = await checkIsDuringTheEventApi()
  if (res.isDuring) {
    message.error('活动期间,功能暂不可用,如有疑问联系运营');
    return
  }
  const url = generateDownloadUrl({ formValues })
  downloadExcelFile(url)
}

function generateDownloadUrl() {
  // 省略
}

Therefore, yesuse caseLayer-by-layer single-test is the best entry point we are looking for. It can not only satisfy us to supplement business documents, but also have the output of single-test modules to ensure our code quality and program stability.

4. Single test practice

After identifying the code modules to be covered by the unit test, the next step is to implement the unit test use case.

As mentioned earlier, writing a unit test itself has a certain threshold, but since you want to write it, you should write a unit test with high maintainability and stability. Otherwise, if the code is slightly refactored, the single test will collapse 😱; or when the code really breaks down, the single test will fail again😅.

According to the previous description, it can be seen thatWe have extremely high requirements for the readability (documentation) and stability of the use cases, and the requirements for the scope of logic tested by the use cases are not high. This criterion will have a great influence on the design trade-offs of subsequent unit test cases.

4.1 Use case design

First of all, we need to determine the entry point of the design use case. At present, the more popular modes in the single test community are nothing more than TDD and BDD:

TDD:Test Driven Developmentit is biased to test whether the results of each function of the function are in line with expectations. Since the implementation of business logic is driven by writing use cases first, the design of use cases is often more technical.

BDD:behavior driven developmentthe process is a branch of the TDD model, the difference is that when conceiving use cases, it is more considered from the perspective of user behavior (user story).

For more differences between the two, you can find more information online, so I won’t repeat them here.For the stability and maintainability of our single test, and we are document-oriented, we naturally chooseBDDmode, which only tests the business behavior logic, and does not pay attention to whether the output of the function function is correct or not (this piece can be guaranteed by the self-test and test brother team at present). In this way, unless the business process changes, the general refactoring or adjustment of the code will not affect the operation of the unit test, and will not cause an avalanche of the unit test.

4.2 Use case structure

In terms of use case structure, in order to cooperate with the original intention of “single test is document” and better cooperate with BDD, we choose between the common AAA (Arrange-Act-Assert) and GWT (Given-When-Then) structures in the community the latter.

No matter AAA or GWT will eventually form a three-stage use case structure, the difference is still that the idea of ​​AAA is more inclined to technical implementation, and GWT is more inclined to business processes. Although the structure is the same, the content of the designed use cases will be very different.

Given-When-Then

Given: A context that specifies and prepares a preset for testing

When: Perform a sequence of operations, the operations to be performed

Then: Get observable results, the assertions that need to be checked

we based onGWTprovides a basic template for the single test, which can be directly used by the students in the group when writing the single test.

function init() {
  const checkIsDuringTheEventApi = jest.fn();
  const downloadExcelFile = jest.fn();
  const exportActivityLog = buildMakeExportActivityLog({checkIsDuringTheEventApi, downloadExcelFile})

  return {
    checkIsDuringTheEventApi,
    downloadExcelFile,
    exportActivityLog
  }
}

describe('spec', () => {
  it('test', () => {
    // Given  准备用例所需的上下文
    const { checkIsDuringTheEventApi, downloadExcelFile, exportActivityLog } = init();

    // When 调用待测的函数
    exportActivityLog()

    // Then  断言
    expect('expect')
  })

For some use cases of validating simple models, passinitA layer of encapsulation is enough for the function. However, for models with complex business logic and many fields, direct initialization using native data is not friendly to the readability of use cases.

describe('spec', () => {
  it('个人卖家未发货的订单,允许进行取消操作', () => {
    // Bad case: 依赖字段较多,这样手动去创造字段数据可读性并不友好
    // 若case较多,这些字段要手动构建多次
    action({
      status: Status.待发货,
      merchantType: MerchantType.个人卖家,
      // ...还有一些其他必传字段
    })
  })
}

For such complex scenarios, we tend to use the builder mode to construct data, ensuring the readability and maintainability of use cases with a small development cost.



describe('spec', () => {
    it('个人卖家未发货的订单,允许进行取消操作', () => {
        // Good case:通过builder实现逻辑的复用和信息的聚焦
        const order = new OrderBuilder()
          .status("待发货")
          .merchantType("个人卖家")
          .build()

        action(order)

    })
})

4.3 Use case description

Since it is intended asdocumentUse, the use case description is also very important. Compared with TDD’s single test of functional functions, our description is completely corresponding to the use case structure of GWT (When is often omitted), we don’t care about the specific technical implementation details, but more about the behavior process of the business described. Think about what the function ultimately wants to do and what purpose it achieves.based onintentiontreat the function under test as a black box, and don’t need to pay attention to the implementation details in the middle, what temporary variables are generated, how many times it loops, what judgments are made, etc., but clearly explain the business process through the use case description.

describe('导出活动日志', () => {
  it('导出时,先查询当前活动状态,若状态是未在进行中,则执行导出操作', () => {
    // 省略...
  })
  it('导出时,若导出数量大于5000条,将不允许导出', () => {
    // 省略...
  })
})

The above 🌰 is an operation to export the activity log. It can be seen that the description of the use case will not be as streamlined as the test function (the input parameter is a, which function must return b after calling), but when exporting the activity, the corresponding The calling process and conditions are described, so that when other people take over this business, they can clearly know what restrictions and operations to do when exporting activity logs through this use case.

4.4 Use Case Assertions

After determining the design idea and structure of the use case, we also made some trade-offs in the verification content of the use case. Aiming at the two camps of classical testing (Classical) and mock testing (Mockist) dominated by the community, combined with the concept of “single test is document”, we have a very strong demand for business process verification, so we chose the latter.

ClassicalThe style is to use real objects and functions as much as possible, so that functions and dependencies are actually executed; in contrast,MockistIt is to do everything possible to mock, and it is recommended to mock all the functions under test that are called. Existence is reasonable, both factions have their own advantages and disadvantages, and there is no certain one is better and the other is worse.

To mock the functions used, on the premise of ensuring the maintainability of the use case (such as not mocking the file path), we need to sort out the dependencies of the functions.Thanks to the implementation of the team’s clean architecture, the usecase layer of the current application has passedDependency InversionThe dependencies are well managed (usecase only depends on entity).

export default function buildMakeExportActivityLog({checkIsDuringTheEventApi,downloadExcelFile}) {
  async function exportActivityLog({count,formValues}) {
    if (count > 5000) {
      message.error('导出文件数量不得超过5000!')
      return
    }
    const res = await checkIsDuringTheEventApi()
    if (res.isDuring) {
      message.error('活动期间,功能暂不可用,如有疑问联系运营');
      return
    }
    const url = generateDownloadUrl({ formValues })
    downloadExcelFile(url)
  }
}

// index.ts
import {checkIsDuringTheEventApi} from '@/services/activity'
import {downloadExcelFile} from '@/utils'
import buildMakeExportActivityLog from './makeExportActivityLog'

export const exportActivityLog = buildMakeExportActivityLog({cancel,printSaleTicket})

can be seencheckIsDuringTheEventApias well asdownloadExcelFileThese two functions are finally passed into the actual function as parameters. One of them will initiate a request, and the other will call the window method to download. Dependency inversion can facilitate our simulation. These two functions will not be actually executed.

function init() {
  const checkIsDuringTheEventApi = jest.fn();
  const downloadExcelFile = jest.fn();
  const exportActivityLog = buildMakeExportActivityLog({checkIsDuringTheEventApi, downloadExcelFile})
  return {
    checkIsDuringTheEventApi,
    downloadExcelFile,
    exportActivityLog
  }
}

use caseThere are often dependent functions that need to initiate a request. We will not actually initiate this request during a single test. Therefore, we should mock all such functions to ensure the speed and stability of our use cases.Of course, we should not be a complete mockist when writing unit tests, and mock endlessly. A better way isCombination of bothotherwise the abuse of mocking will lead to more cumbersome writing of single tests (because of the need to mock all the function implementations or scenarios called), and it will be awkward to write real code (all external functions depend on inversion).

Whether a use case is correct or not ultimately depends on the final assertion, so how do we assert it? As we have always emphasized, we measure logical behavior, so what we need to assert is whether a certain behavior is executed Or whether it has achieved any purpose. Combined with the previous mock, we can capture the calling of the function. For the above function that initiates the cancellation of the refund, the example of the assertion is as follows:

describe('导出活动日志', () => {
  it('导出时,先查询当前活动状态,若状态是未在进行中,则执行导出操作', () => {
    // 省略...
    expect(downloadExcelFile).toBeCalled()
  })

  it('导出时,若导出数量大于5000条,将不允许导出', () => {
    // 省略...
    expect(downloadExcelFile).not.toBeCalled();
  })
})

As above, the content of the assertion is not the implementation details of the function, such as whether the parameters are correct, but only asserts whether the behavior is executed. It can try to ensure that if the code is refactored, the single test case can still run robustly without modification , which only depends on changes in requirements to make changes. At the same time, in order to maintain the stability of the use case,For a single use case we usually only execute the assertion once (single responsibility), The content of the assertion corresponds strictly to the “Then” part of the description.

5 Conclusion

Merchant to “A single test is a document” The concept is the direction of landing, and certain trade-offs have been made in code design and use case conception, structure, assertion, description, etc., and finally a relatively good balance has been achieved in terms of writing cost, stability, and readability of use cases .

At present, various projects in the group have gradually accumulated hundreds of use cases. When the team supports each other or reviews by themselves, they can know what the logic is doing through these use cases. When modifying these requirements, they can also know the basics as soon as possible through test cases. With the guarantee of single-testing for business logic, it is more confident to change the code, and the code structure is more reasonable. After everyone is gradually familiar with the single test, the follow-up will gradually achieve the single test coverage of functional functions, UI, etc., and everyone will work together to ensure the stable development of the merchant’s front-end business.

Reference article:

“Clean architecture” and the road to reconstruction of the front-end of merchants:

https://mp.weixin.qq.com/s/Sgr6El88eqjCDaRFxIVFQA

The Difference Between TDD and BDD:

https://joshldavis.com/2013/05/27/difference-between-tdd-and-bdd/ https://lassala.net/2017/07/20/test-style-aaa-or-gwt/

jest documentation:

https://jestjs.io/zh-Hans/docs/getting-started


*Text/Chun Meng

Pay attention to Dewu technology, and update technical dry goods every Monday, Wednesday and Friday nights at 18:30

If you think the article is helpful to you, please comment, forward and like~

#practice #single #testing #frontend #business #merchants #Dewu #Technologys #personal #space #News Fast Delivery

Leave a Comment

Your email address will not be published. Required fields are marked *