Talking about single test from head to toe — on effective unit test (Part I)

Time:2021-9-23

1、 “Correct name” for unit test
I used to think that unit testing was for a function. Any test that comes out of a function is not a unit test.
In fact, the definition of “unit” depends on yourself. If you are using functional programming, a unit is most likely to refer to a function. Your unit test will call this function with different parameters and assert that it returns the expected result; In object-oriented languages, everything from a method to a class can be a unit (from a single method to an entire class can be a unit). Intention is very important (“intention” is mentioned for the first time in this article, it is very important)
We have unit testing, incremental testing, integration testing, regression testing, smoke testing and so on. Seeing this phenomenon of “a hundred schools of thought contend”, Google created its own naming method, which is only divided into small-scale test, medium-sized test and large-scale test.
·Small test, for the test of a single function, pay attention to its internal logic and mock all required services.
Small tests bring excellent code quality, good exception handling, and elegant error reporting
·Medium scale test to verify the interaction between two or more developed module applications
·Large scale testing, also known as “system testing” or “end-to-end testing”. Large scale tests run at a higher level to verify how the system works as a whole.
Talking about single test from head to toe -- on effective unit test (Part I)
Conclusion: our unit test can write cases for a function or string them according to the call relationship of the function.
2、 Pyramid model
Before the pyramid model, ice cream model was popular. It includes a large number of manual tests, end-to-end automated tests and a small number of unit tests. The consequence is that with the expansion of products, the manual regression test time is longer and longer, and the quality is difficult to control; Automated cases fail frequently. Each failure corresponds to a long function call. What’s wrong? The unit test is poor and basically useless.
Talking about single test from head to toe -- on effective unit test (Part I)
Mike Cohn put forward the concept of “test pyramid” in his book succeeding with agile. This metaphor is very vivid. It lets you know at a glance that testing needs to be layered. It also tells you how many tests need to be written for each layer.
Testing the pyramid itself is a good rule of thumb. We’d better remember two things Cohn mentioned in the pyramid model:
·Write tests of different granularity
·The higher the level, the fewer tests you should write
Talking about single test from head to toe -- on effective unit test (Part I)
At the same time, our understanding of pyramids must not stop here, but further understand:

I understand the pyramid model as – the ice cream has melted. That is, in theory, the top “manual test” should be automated, melt down, and give priority to melting into unit test. What cannot be covered by unit test will be placed in the middle layer (layered test), and what cannot be covered will be placed in the UI layer. Therefore, there is no case in the UI layer, and the running is slow and unstable. According to gang leader Qiao, I call it automated testing regardless of unit testing or layered testing. Then all automated cases should be regarded as a whole. Cases should not be redundant. If unit testing can cover, we should remove this case from layered or UI.
The lower level tests involve less relevant content, while the higher level tests cover a wider range. For example, unit testing focuses on only one unit and nothing else. Therefore, as long as a unit is written, the test can pass; Integration testing can only be tested by assembling several units together. The prerequisite for passing the test is that all these units are written, and the cycle is obviously longer than that of unit testing; The system test needs to connect all modules of the whole system and prepare all kinds of data before it can pass.
In addition, because too many modules are involved and any module is adjusted, it may destroy the high-level test. Therefore, the high-level test is usually relatively fragile. In actual work, some high-level tests will involve external systems. In this way, the complexity is constantly increasing.
III   Why single test
We can’t avoid this problem. News is one of the main forces of the R & D model reform, so the top-down promotion makes this problem less difficult: do what you do. There are so many reasons for not doing it:
(collected real voice)
·   Unit testing wastes too much time
·   Unit testing is just proving what these code does
·   I’m a great programmer. Can I skip unit testing?
·   Later integration tests will catch all bugs
·   The cost efficiency of unit testing is not high. I have written all the tests, so what do testers do?
·   The company asked me to write code, not test
·   It’s not my job to test the correctness of the code
Significance of unit testing
·Unit testing is very important to the quality of our products.
·Unit test is the lowest level of all tests. It is the first and most important link. It is the only test that can ensure 100% code coverage. It is the basis and premise of the whole software testing process. Unit test prevents out of control due to too many bugs in the later stage of development, and the cost performance of unit test is the best.
·According to statistics, about 80% of errors are introduced in the software design stage, and the cost of correcting a software error will rise with the progress of the software life cycle. The later the error is found, the higher the cost of repairing it, and the trend is increasing exponentially. As a coder and the main executor of unit testing, he is the only one who can produce defect free programs, which no one else can do
·Code specification, optimization, testability code
·Rest assured reconstruction
·Automated execution of three thousand times
The following figure shows the statistical data from Microsoft: the bug is found in the unit test phase, which takes an average of 3.25 hours. If it is missed in the system test phase, it takes 11.5 hours.
Talking about single test from head to toe -- on effective unit test (Part I)
The following figure aims to illustrate two problems: 85% of defects are generated in the code design stage, and the later the bug discovery stage is, the higher the cost and the higher the index level. Therefore, you can find bugs in early unit testing, save time and effort, and do it once and for all.
Talking about single test from head to toe -- on effective unit test (Part I)
Is unit testing particularly time-consuming?
We can’t just focus on the time-consuming of the single test stage.
I interviewed the development of news client and background. First of all, I’m sure that the single test will increase the amount and duration of development;
Talking about single test from head to toe -- on effective unit test (Part I)
In the art of unit testing, a case is mentioned: two teams with similar development capabilities are found to develop similar requirements at the same time. The duration of the single test team in the coding phase has doubled, from 7 days to 14 days. However, the team’s performance in the integration test phase is very smooth, the number of bugs is small, and bugs are located quickly. The final effect, overall delivery time and number of defects are the least for the single test team.
Talking about single test from head to toe -- on effective unit test (Part I)
Single test, existence is reasonable. On the one hand, we need to put the single measurement in the whole iteration cycle to observe its effect; On the one hand, writing a single test is also a technical activity. Students who write well have less time and high code quality (that is, it doesn’t mean that they can write a single test well after writing a single test)
Who will write the single test?
·Develop students to write a single test
·Test students have the ability to write a single test. The focus is on developing scaffolding, layered testing / end-to-end testing
Increment or stock
·Single test case for incremental code
·When there is a large-scale reconstruction of the stock code and the quality of the latter exposes great risks, it is a good time to promote the completion of the single test
4、 Phase of unit test
1、 In a broad sense, we refer to the organic combination of these three parts:
·code review
·Static code scanning
·Unit test case writing
2、 Combined with the practice of journalism, I divided the growth process of single test into four objectives, namely:
·Can write, all staff can write
·Well written, pay attention to testability problems and solve them on a pilot basis
·Identify testability problems and skillfully use reconstruction methods for reconstruction; Identify code architecture design issues; Case and business code are written synchronously
·TDD。 But this goal is an expectation, not a goal that must be achieved.
Talking about single test from head to toe -- on effective unit test (Part I)
As of the press day, the news is in the third stage, that is, each iteration can produce high-quality cases, with high number coverage and demand coverage; Focus on testability and always focus on refactoring.

5、 Indicators of unit test
It’s quite embarrassing. There are few direct indicators to measure the effect of single test. We are often asked, “how to prove the role of your news single test?”
·Bug indicators (indirect indicators): the trend of the total number of bugs in successive iterations, the trend of new bugs in iterations, and the bug rate per thousand lines
·The demand coverage of single test (more than 50%) and the coverage of participants (more than 80%)
·Single test case total number trend, code line increment trend
·Line coverage of incremental code (access layer 80%, client 30%)
·Single function cycle complexity (less than 40), number of single function code lines (less than 80), number of scanning alarms
On the premise of continuous high throughput of iteration requirements, take the data of news IOS as an example:
Talking about single test from head to toe -- on effective unit test (Part I)
Talking about single test from head to toe -- on effective unit test (Part I)
Talking about single test from head to toe -- on effective unit test (Part I)
Talking about single test from head to toe -- on effective unit test (Part I)
6、 Go unit test framework selection
Basic model selection: verify + gomonkey
Additional: httptest + sqlmock
Talking about single test from head to toe -- on effective unit test (Part I)
Talking about single test from head to toe -- on effective unit test (Part I)
premise
·Test files to_ Test.go ends in the same directory as the tested file
·Test function. The function name starts with test, and the first character after it must be an uppercase letter or underscore, such as testparsereq_ CorrectNum_ TableDriven
·Test function with parameter ttesting.T; For bench tests, the parameter is Btesting.B
·Run the command line. My article has an in-depth explanation: go test command line

Testify general usage
https://github.com/stretchr/t…
Testify is written based on gotesting, so the syntax and execution command line are fully compatible with go test
Support a large number of efficient APIs, such as:
Assert. Equal: the conventional comparison is to replace the two with [] byte to strictly compare them
Assert. Nil: when the judgment object is nil, sometimes it is also used when err is null
Assert. Error: judge the specific type and content of err
Assert. Jsoneq: This is useful when comparing maps; Or when comparing structs, they will be converted to maps first. When using this API for comparison, as in the following example, I encapsulated the proposed method to convert structs into strings (JSON):
Talking about single test from head to toe -- on effective unit test (Part I)
Talking about single test from head to toe -- on effective unit test (Part I)

·   Support suite and use case set management
·   At runtime, you can specify use case set execution
Talking about single test from head to toe -- on effective unit test (Part I)
·   It has its own mock tool, but only supports mock of interface methods, and its usage is relatively complex
· table-driven
Talking about single test from head to toe -- on effective unit test (Part I)
Gomonkey usage (blue font indicates common)
https://github.com/agiledrago…
https://studygolang.com/artic…

·It supports driving a pile for a function
·Supports driving a stake for a member method
·It supports driving a stake for a global variable
·It supports driving a pile for a function variable
·It supports driving a specific pile sequence for a function
·It supports driving a specific pile sequence for a member method
·It supports driving a specific pile sequence for a function variable
·Define a series of stubs in the form of table driven
Note that for the stub and go test command line of the inline function, you must add parameters to take effect. See official documentation. Therefore, my command line will add – gcflags = all = – L by default.
Talking about single test from head to toe -- on effective unit test (Part I)
I set up some GoLand code templates and put them in the attachment.
Applyfunc is an external function stub (non class method)
/*   Usage: gomonkey. Applyfunc (name of stub function,   (signed by stub function)   Function return value
    * example:
    patches := gomonkey.ApplyFunc(fake.Exec, func(_ string, _ …string) (string, error) {
    return outputExpect, nil
                   })
 */

patches := gomonkey.ApplyFunc(lcache.GetCache, func(_ string) (interface{}, bool) {
return getCommentsResp()
})
defer patches.Reset()
Applymethod is a stub to a class function. Note here that the method to be stub is a private method. Gomonkey cannot be found through reflection. There are two solutions: 1) use the enhanced version of gomonkey; 2) Instead of stubbing it, I choose to walk into this function. This topic will be discussed in the later topic of mock.
/*   Usage: gomonkey. Applymethod (reflection class name,   (signed by stub function)   Function return value
    * example:
    var s *fake.Slice
    patches := ApplyMethod(reflect.TypeOf(s), “Add”, func(_ *fake.Slice, _ int) error {
                return nil
            })
 */
 
var ac *auth.AuthCheck
patches := gomonkey.ApplyMethod(reflect.TypeOf(ac), “PrepareWithHttp”, func(_ auth.AuthCheck, http.Request, …auth.AuthOption) error {
return fmt.Errorf(“prepare with nil object”)
})
defer patches.Reset()
Applymethodseq returns different results for the same stub function
/*   Usage: gomonkey. Applymethodseq (reflection of class), “stub function name”,   Return structure);
     Params {Info1}, the return value list of the stubbed function is in brackets;
     Times is the number of effective times
    * example:
    e := &fake.Etcd{}
    info1 := “hello cpp”
    info2 := “hello golang”
    info3 := “hello gomonkey”
    outputs := []OutputCell{
         {Values: Params{info1, nil}},
         {Values: Params{info2, nil}},
         {Values: Params{info3, nil}},
      }
      patches := ApplyMethodSeq(reflect.TypeOf(e), “Retrieve”, outputs)
      defer patches.Reset()
 */
conn := &redis.RedisConn{}
patch1 := gomonkey.ApplyFunc(redis.NewRedisHTTP, func(serviceName string, _ string) *redis.RedisConn {
conn := &redis.RedisConn{
redis.RedisConfig{},
&redis.RedisHelper{},
}
return conn
})
defer patch1.Reset()
 
// mock redis data.   Returns null and non null conditions
outputCell := []gomonkey.OutputCell{
{Values: gomonkey.Params{“12”, nil}, Times: 1},
{Values: gomonkey.Params{“”, nil}, Times: 1},
}

patchs := gomonkey.ApplyMethodSeq(reflect.TypeOf(conn.RedisHelper), “Get”, outputCell)
defer patchs.Reset()
  Take these examples first. The details can be fully obtained in the link article above.
Here is a supplementary point. To stub a class method, you must find the real class (structure) corresponding to the method, for example:
//The function under test has the following paragraph. We want to stub the get method. Just find the class corresponding to the get method
readCountStr, _ := conn.Get(redisKey)
if len(readCountStr) == 0 {
return 0, nil
}
Locate Conn, which is a redisconn type struct
 
type RedisConn struct {
RedisConfig
*RedisHelper
}
So the first time I used gomonkey.appleymethod, I wrote this:
 
patches := gomonkey.ApplyMethod(reflect.TypeOf(RedisConn),”Get”, func(_ redis.RedisHelper,_ string, _ []string) ([]string, error){
return info,err_notNil
})
defer patches.Reset()

Wetest editor’s note: that’s all for the first part. I’m sure you haven’t seen enough. In the next part, we’ll talk about mock and how not to abuse mock. Let’s take a quick look at it together ~ “single test from head to toe — Talking about effective unit testing (Part 2)”