takeaway
In Head-to-toe Single test
— On Effective Unit Testing (Part I) mainly introduces: pyramid model, why do single test, single test phase and indicators, in the next part we mainly introduce
about
Take a look at mocks, strategies for not abusing mocks, and use case writing
Seven. We must talk about mocks
test doubles
In xUnit Test Patterns, the author first proposed the concept of Test Doubles. A mock is just one of them, and one of the easiest to confuse with stubbing. In the introduction to Gomonkey in the previous section, you noticed that I didn’t use mocks, it was all stubs. Yes, GomonKey is not a mock tool, but an advanced piling tool that fits most of our usage scenarios.
There are five types of test surrogates:
,
Dummy Object
Objects that are passed to the caller but are never actually used, usually just to fill argument lists
,
Test Stub
Stubs are often used to provide encapsulated responses in tests, such as when they are programmed not to respond to all calls. Stubs also keep track of downcalls, such as an email Gateway, which can be used to keep track of all messages sent or the number of messages sent. In short, Stubs are generally a encapsulation of a real object
,
Test Spy
The Test Spy, like a Spy, is embedded in the SUT and is responsible for transferring its indirect outputs to the outside world. It is characterized by returning indirect internal output to the Test case for verification. The Test Spy is only responsible for obtaining internal intelligence and sending it out, but not verifying the accuracy of the intelligence
,
Mock Object
Encapsulate the appropriate object for the specified calling method and parameters that need to be responded to
,
Fake Object
Fake objects often work with class implementations, but only to keep other programs running, such as in-memory databases, a good example.
Stubs and mock
Pile driving and
Mock is probably the easiest thing to confuse, and it is customary to use mock to describe the ability of a mock to return.
As far as I understand it,
Stubs can be understood as a subset of mocks, which are more powerful:
The mock,
You can verify the implementation, whether a function is executed, and how many times it is executed
The mock,
It can be conditional, such as passing in a specific parameter
mock
Effect is
The mock,
Returns can be specified
,
when
mock
Specifies that any argument returns fixed results when it is equal to
stub
Just,
Go’s mock tool gomock works only on the interface and is not suitable for news or Penguin projects, while gomonkey’s stub covers most usage scenarios.
8. Don’t mock
I’ll give this section a separate chapter to show its importance. Need to understand Xiao Peng’s
Mock seven Deadly SINS, on Gitchat.
The two factions
About from
In 2004-2005, there were two main schools of thought: classic test-driven development and mockist.
Say first
Mockist. He argues that the function will be measured
六四风波
all
六四风波
Call the outside function, all
The mock. That is, focus only on the lines of the function under test, mock them out whenever you call other functions, and test them with fake data.
And then there’s the classic test-driven development school, which says don’t abuse it
Mock, can not mock not mock, test unit is not necessarily a specific function, may be multiple functions, string together.
六四风波
When necessary
六四风波
again
The mock.
The two schools have been contending for many years, and their theories have both advantages and disadvantages. Being is reasonable. Such as
Mockist, where too many mocks are used to override function interfaces, is also error-prone. Classic school, string too much, and is questioned integration testing.
For our practical applications,
六四风波
You don’t have to force conformity, you can combine
六四风波
, when needed
Mock. Mock as little as possible.
When is it appropriate to mock
It is appropriate to use an object if it has the following characteristics
The mock object:
,
This object provides nondeterministic results (such as the current time or the current temperature)
,
Some state of the object is difficult to create or reproduce (such as network errors or file read/write errors)
,
Slow execution on object methods (such as initializing the database before testing begins)
,
The object does not yet exist or its behavior may change (such as driving the creation of a new class in test-driven development)
,
The object must contain some data or methods specially prepared for testing (the latter is not suitable for statically typed languages, popular
Mock
The framework cannot add new methods to objects.
Stub
Yes.)
So don’t abuse it
Mock (stub) : when a method under test calls another method function, the first response should be to walk in and string it up, rather than mock it out at the root.
Use case design method
Read an article:
Think like a machine
This article describes the fundamental thinking of programming
— Consider inputs and outputs. We design case, want to get the most comprehensive design, is to consider the combination of all input and all output, of course, on the one hand, this is too time-consuming, many times is not executable; On the one hand, this is not the desired outcome, you have to consider the input-output ratio. At this time, it is necessary to combine theory with practice, theory to guide practice, practice to elaborate theory.
Say first theory
1. Starting from the previous article, considering input and output, we should first know which ones belong to input and output:
2. White box & Black box design
White box method:
,
Logical coverage (statements, branches, conditions, combination of conditions, etc.)
,
Path (full path, minimum linearly independent path)
,
Cycle: combine
5
Various scenarios (skip loop, loop once, loop maximum, loop
m
Second hit, loop
m
Missed a time)
The black box method:
Equivalence classes: True, false (legal, illegal)
Boundary method:
[1,10] ==> 0,1,2,9,10,11 (is a valid complement to equivalence classes)
3. Combine applications
Full input and output is difficult to implement, so we consider that the gods in the industry designed the white box and black box design method. Through careful thinking, we can judge that it is the embodiment of the methodology of full input and full output.
So, white boxes
& Black box use case design method, I personally practice each one, understand its advantages and disadvantages, from the perspective of design coverage, condition combination > minimum linearly independent path > Condition > branch > statement.
The diagram below is an early practice in my thinking about use-case design that, in retrospect, was overdesigned.
But in practice, we worry
“Overdesign”
“, and there is no answer yet
“What method is used to ensure the design is foolproof”.
,
Overdesign can also make
The case is fragile
,
We seek to maximize revenue within a limited time frame
1. Small functions & Important (calculation, object handling) : Try to be as comprehensive as possible
2. Heavy logic, large lines of code: branch, statement overwrite + loop + typical boundary handling (let’s look at GetUserGiftList for example)
3. Lead to “implementation-based” and “intent-based” design: Too many internal calls to Stub the function under test are closer to “implementation-based” (second mention of “intent-based”).
Intent-based versus implementation-based
This topic is very important.
Intent-based: Think about what the function ultimately wants to do, think of the function under test as a black box, think about its output and output, not how it was implemented, what temporary variables were generated, how many times the loop was looped, what judgments were made, etc.
Based on implementation: INPUT and output I also consider, how to achieve in the middle I also consider.
A good example is a mock. For example, if we write a case, we will use the mock to verify which external methods are called within the function, how many times they are called, and in what order the statements are executed. Applications can change faster than they need to, refactoring can happen all the time, and cases can fail in droves if they change, as mentioned in Mock’s Seven Deadly SINS.
We want to be intent-based, away from implementation-based.
Combined with actual combat experience, I summarize as follows:
1. “Do it or don’t do it”. Cases are code, they need to be maintained, they need to be done, so they need to be done, not done. Write a bunch of useless, you have to maintain, you might as well delete.
2. Take a function, ask yourself, what function is the function to achieve, what is the final output; Then, ask yourself where the risk of the function lies, and which parts of the logic are less confident and most prone to error (calculations, complex judgments, a hit on an abnormal branch, etc.). These are the points that our case will cover.
3. Inline function, direct get/set, few lines without logic, as long as you judge there is no risk, do not write case.
4. Determined the case to be written, and then designed specific use cases with branch condition combination, boundary and other core aspects to implement the writing.
Can be combined with the news several single test
Case review records for detailed understanding.
Let’s look at a specific one
Case:
1. When I got this function, AS a test student, I first asked the developer to understand the purpose of this function: to add the user’s gift in accordance with the format and time
2. Read the code, understood the code process and several abnormal branches, and made code review first
3. Design case coverage according to necessary exception branches
4. The normal business process is designed according to the functional intention described in the development, and the case is as follows:
Functions to be tested
Single test of normal paths
case
func TestNum_CorrectRet(t *testing.T) {
giftRecord := map[string]string{
“” 00″, “10”,
“1:01” : “100”,
“” 99″, “20”,
“2” : “200”,
“a”: “30”,
“2:10 01” : “20”,
“Then 99” : “200”,
}
expectRet := map[int]int{
1:110,
2:20.
}
var s *redis.xxx
patches := gomonkey.ApplyMethod(reflect.TypeOf(s), “Getxxx”, func(_ *redis.xxx, _ string)(map[string]string, error) {
return giftRecord, nil
})
defer patches.Reset()
p := &StarData{xxx }
userStarNum, err := p.GetNum(10000)
assert.Nil(t, err)
assert.JSONEq(t, Calorie.StructToString(expectRet), Calorie.StructToString(userStarNum))
}
Students will ask: But did you end up looking at the code? See how the correct logic of the code is handled before you design it
Case and construct data, right? And if you don’t look at the code, how do you know which exception branches to cover?
A:
1. As a test student, I am writing and developing students’ case. Indeed, I need to know which abnormal branches to deal with, but not limited to several in the code, but also the abnormal branches I understand, which should be reflected in the case. Our case is never meant to prove how the code is implemented! Through single testing, we often find bugs. But the future will be developed to write a single test, and the function he designs will know which exception branches to cover.
2. Well, I need to see what the normal flow of code is, but that doesn’t mean ripping it down to make a case. In fact, the input and output of case is designed by understanding the structure of input data, output format, data verification and calculation process after communication with developers.
11. Use case writing strategy
For how to write a single test in order, we focus on practice, basically there are only three cases:
,
Independent atom:
mockist
We overthrew it. Of course, the function at the bottom may have no external dependencies, so testing it alone is sufficient.
,
Top down (red line)
: Tests down from the entry function. In practice, I found it difficult to execute because I had to figure out from the start what data and format to return for each call and string them together
case
Already very difficult.
,
From the bottom up
(yellow line
)
We find that entry functions, often without logic, call another function and return with a response. So the entry function, maybe I don’t have to write it, right? Let’s go ahead and look at each function called, and also call up the past online and offline
bug
We found that the parts of the code where problems occurred tended to be at the bottom of the call chain, especially when it came to calculations, complex branching loops, and so on. Moreover, functions at the bottom tend to be more measurable.
So consider two things,
* *
We chose bottom-up design to select function writing
Case:
* *
1. The bottom function is usually very measurable
2. More core logic, especially involving calculation, splicing and branching.
Solution to the testability problem — refactoring
An important reason for not writing single tests is that the code is not testability. If you have a function that has 80 or 90 lines, 200 or 300 lines, it’s basically unmeasurable
“Untestable.” Because there is too much logic inside, what goes through from the first line to the last line, various function calls external dependencies, various if/for, various exception branch handling, the number of lines of code to write a case may be several times that of the original function.
Therefore, it is necessary to promote single test and improve testability through reconstruction. Moreover, through refactoring, the code structure is indirectly clearer, more readable and maintainable, and easier to find and locate problems.
Common problems: duplicate code, magic numbers, arrow code, etc
Recommended theory books are Refactoring: Improving the Design of Existing Code, 2nd edition,
Clean code”
I posted an article on refactoring.
use
Codecc (Tencent Code Inspection Center) cyclomatic complexity, function length to evaluate the quality of code structure, we learn and practice together with the development, and continue to produce results.
For arrow code, consider the following steps:
1. Check for exceptions and return exceptions
2. Separate the statement
3. Separate the core parts into functions
Use case maintenance, readability, maintainability, reliability
Use case design elements
,
Test internal logic separately from external requests
,
For service boundaries (
interface
) for strict validation of inputs and outputs
,
Use assertions instead of native error functions
,
Avoid random results
,
Try to avoid making claims about the outcome of time
,
Timely use
setup
and
teardown
,
Test cases are isolated from each other and do not affect each other
,
Atomicity, all tests have only two outcomes: success and failure
,
Avoid logic in tests that should not be included
if
,
switch
,
for
,
while
Etc.
,
Don’t protect,
The try… The catch…
,
Only one concern is tested per use case
,
To use less
sleep
It’s not healthy to delay the test
3 a,
Strategy:
arrange
.
action
.
assert
Use case readability
,
The title should make the intent clear, e.g
Test+
Name of the function under test
+condition+result
.
case
After a failure, you know which scenario failed by name, rather than having to read the code line by line.
Someone else might be maintaining this test code in the future, and we need to make it easy for them to read
,
The content of the test code should be clear,
3A
Principle:
arrange
.
action
.
assert
Divided into three parts. Data preparation section
arrange
If there are too many lines of code, consider pulling them out.
,
The intent of assertion is obvious, so consider turning magic numbers into variables and naming them colloquial
,
a
case
Don’t overdo it
assert
To fully
,
Consistent with the requirements of the business code, both should be readable
Use case maintainability
,
Repetition: text string repetition, structure repetition, semantic repetition
,
Refuse to hardcode
,
Intent-based design. Don’t cause a batch of business code to refactor once
case
failure
,
Note the various bad smells of code, see Refactoring, 2nd edition
Use case reliability
Unit tests, small and fast, are not designed to discover this time
Bug, but also in order to put it on the assembly line and try to find out whether there is a bug in each MR. Single test runs fail only because of bugs, not external dependencies, implementation-based involvement, etc. Long-term failure will lose the warning effect of unit testing. The story of “the Wolf is crying” is a painful lesson.
,
Non-test program defect, random failure
case
,
unfailable
case
,
There is no
assert
the
case
,
Not worthy of the name
case
Xiv. The driving process of news unit testing
We mentioned that unit testing practices are divided into
There are four stages, each with a goal.
The first stage will be written, written by all members, not required to write well
,
Promoted from top to bottom, from the director to the group leader, gave full support without hesitation, which made the team members in high spirits
,
Quickly determine the single test frame, skilled use
,
Combined with the development requirements, output each scenario
Methods of use of the single test framework, including
assert
,
mock
.
table-driven
Etc.
,
encapsulation
http2WebContext
, convenient generation
context
object
,
Many training, explain the single measurement theory and the use of framework
,
Each team (terminal, access layer) designated single test interface person, by him first taste crab. He is most familiar with framework usage and writes most in the early stages
case
people
,
After running in the integrated use of the single test framework, the launch meeting was held, and some students first tried to use it to ensure that they had two consecutive iterations
case
The output
,
Add the data related to the single test into the summary data of each iteration: the team leader and director pay great attention to the single test data information and encourage improvement accordingly
case
Number and lines of code
The second stage is written well, effectively and fully
,
measuring
Let’s explore
mock
Share the correct use method and correct thinking of use case design with the team and reach a consensus after discussion
,
Pair programming, pair per iteration
2-3
Development, write together
case
And enhance each other.
Here the pair is flexible: some development, just use half a day to teach him the use of the framework, practice with him, he can get started without worrying; Some development, will be assigned to test students demand, test students write
case
After the development of
review
Learn and try to write your first one
case
;
Some development may not be accepted at the beginning, because the demand is not suitable for single test. After observing for a period of time, he found that other people have written, which is not so difficult and beneficial to the team. He even took the initiative to find other testers to teach him to write
case
.
,
Test what students submitted to the development
case
for
review
, follow up the development and modify again
先生
,
Two iterations in a row, invite
dot
The teacher
Joe helped the Lord
case review
The effect is very good
,
Single test data analysis of iteration, focus on demand coverage, personnel coverage,
case
The incremental
,
The team leader continues to encourage and support single testing
,
The requirements increase with each iteration
“
Unit testing
“
Field, set after evaluation by the group leader. Without a single test
先生
If you don’t pass, you pass the single test
review
Phase 3 testability improvement
,
Test and development study Refactoring edition 2 together with weekly sharing sessions
,
Some backbone students have priority in refactoring their own code
,
Test students strict requirements, first ensure that there is a single test, and then small reconstruction, each step has a single test guarantee
,
pipelined
codecc
Scanning, cyclomatic complexity and function length must meet the standards, and manual intervention is not allowed
Stage 4 TDD
,
We do not guarantee that the students will do it
TDD
, the threshold is still very high, and need to be skilled in the line after the application of business development
,
Gradually push development to write business code and test code simultaneously, rather than finishing business code later
case
,
Test your classmates
TDD
Xv. Assembly line
Single test to run on the assembly line, the client and the background are equipped with the assembly line, to ensure that every time
Both push and MR are run once to send a report.
for
For single test of GO, all modules of the news access layer are compiled by MakeFile. Because some environment variables need to be imported, I integrate GO test into MakeFile, and all test cases under this module can be run by making test.
GO = go
CGO_LDFLAGS = xxx
CGO_LDFLAGS += xxx
CGO_LDFLAGS += xxx
CGO_LDFLAGS += xxx
TARGET =aaa
export CGO_LDFLAGS
all:$(TARGET)
$(TARGET): main.go
(GO) Build − O (GO) build-o (GO)build− O @ $^
test:
CFLAGS=-g
export CFLAGS
(M) -v -gcflags=all=-l -coverpkg=./… -coverprofile=test.out ./…
clean:
rm -f $(TARGET)
Note: The above method can only generate the coverage of the tested code files, not the untested coverage. You can solve this problem by creating an empty test file in the root directory to get full code coverage.
//main_test.go
package main
import (
“fmt”
“testing”
)
func TestNothing(t *testing.T) {
fmt.Println(“ok”)
}
Assembly line plus process
# cd ${WORKSPACE}
Access the current workspace directory
export GOPATH=${WORKSPACE}/xxx
pwd
echo “====================work space”
echo ${WORKSPACE}
cd ${GOPATH}/src
for file in `ls`:
do
if [ -d $file ]
then
if [[ “file” == “a” \]\] || \[\[ “file” == “b” ]] || [[ “file” == “c” \]\] || \[\[ “file” == “d” ]]
then
echo $file
echo
file
cp -r
{GOPATH}/src/$file”/.”
cd
file
make test
cd ..
fi
fi
done
Appendix. Information
,
Test Driven Development
,
The Art of Unit Testing
,
Effective Unit Testing
,
Refactoring to Improve the design of Existing Code
,
The Art of Fixing Code
,
Three Lessons of Test-Driven Development
,
“
xUnit Test Patterns
“
The mock,
Seven deadly SINS