This is a creation in Article, where the information may have evolved or changed.
Preface
To write good test code, you must be proficient in the relevant testing framework. For Golang programmers, there are at least three test frameworks that need to be mastered:
Through the previous article "Gostub Framework Use Guide" learning, we are familiar with the basic use of gostub framework, you can gracefully on global variables, functions or process piling, improve the unit Test level.
Although the GOSTUB framework has solved many scenarios of function piling, but for some complex situations, it can only be despair:
The database read operation function interface Readdb is called several times in the measured function, and the database is Key-value type. The measured function first READDB the value of a parent directory and then reads the values of several subdirectories in the For loop. In multiple test cases there is a need to pile READDB to render different behavior in multiple invocations, that is, the value of the parent directory is different from the value of the subdirectory, and the values of the subdirectories are not equal
The same underlying operation function is called multiple times in the measured function, such as exec.command, and the function parameters are both command and command parameters. The measured function first creates an object, then queries the state of the object, and deletes the object when the object's state is not up to expectations, where the query object is an important operation and is typically retried several times. In multiple test cases there is a need to pile exec.command into different behaviors in multiple calls, that is, creating objects, querying object state, and deleting objects are not expected to return values.
...
In view of the complex situation that the GOSTUB framework does not apply, this article will carry on two development to this framework, the graceful change does not apply for the application, enhances the gostub frame the adaptation ability.
Interface
According to the open and close principle, we should add two interfaces to deal with complex situations by adding interfaces:
- Function interface
- Method interface
For complex cases, there is a different behavior for multiple invocations of a function, that is, there are several list of return values. Obviously the user should specify an array slice when piling []output, so what should the element Output of the array slice be?
The size of the return value list for each function is not deterministic and the return value type is not uniform, so the output itself is an array slice and the output element is interface{}.
The output then has the following definition:
type Output []interface{}
The Declaration for the function interface is as follows:
func StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs
The declaration for the method interface is as follows:
func (s *Stubs) StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs
However, there are two scenarios:
- When the piling function is in a retry call scenario, there are multiple adjacent values in the outputs
- Multiple adjacent values in outputs are the same when the piling function returns to normal and returns a list of values in the same scenario
Repetition is the root of all evils, we maintain 0 tolerance, so we introduce the times variable into output, so the definition of output evolves to:
The output then has the following definition:
type Values []interface{}type Output struct { StubVals Values Times int}
Interface uses
Scenario One: Read the database multiple times
Suppose we read the database 3 times in a function f, such as calling 3 function Readleaf, which reads 3 different value through 3 different URLs. The readleaf is defined in the DB package, with the following examples:
var ReadLeaf = func(url string)(string, error) { ...}
Assuming that the stubs object has not been generated before the function is piling, the piling code for the scene that covers the 3 read database is as follows:
info1 := "..."info2 := "..."info3 := "..."outputs := []Output{ Output{StubVals: Values{info1, nil}}, Output{StubVals: Values{info2, nil}}, Output{StubVals: Values{info3, nil}},}stubs := StubFuncSeq(db.ReadLeaf, outputs)defer stubs.Reset()...
Description: When times is not specified, the time value is 1
Scenario Two: The underlying operation has retry
Suppose we call in a function f 3 of the underlying operation function, such as the call 3 command function, that is, the first call to create the object, the second call to query the state of the object, the third time the state is not expected to drop the deletion of the object, where the second call to improve correctness, made 10 attempts. The command is defined in the exec package and belongs to the library function, we cannot directly pile, so we have to do two packages in the Adaptive Layer Adapter package:
var Command = func(cmd string, arg ...string)(string, error) { ...}
Assuming that the stubs object has been generated before the function is piling, the piling code for the scene that covers the first 9 unsuccessful attempts and the 10th attempt succeeds is as follows:
info1 := "..."info2 := ""info3 := "..."outputs := []Output{ Output{StubVals: Values{info1, nil}}, Output{StubVals: Values{info2, ErrAny}, Times: 9}, Output{StubVals: Values{info3, nil}},}stubs.StubFuncSeq(db.ReadLeaf, outputs)...
Interface implementation
function Interface Implementation
The implementation of the function interface is simple, and the direct delegate method interface is implemented:
func StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs { return New().StubFuncSeq(funcVarToStub, outputs)}
The purpose of providing a function interface is to use the interface before the stubs object is built.
Method Interface Implementation
Let's review the declaration of the Method interface:
func (s *Stubs) StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs
The implementation of the method interface is relatively complex and requires two powerful functions, reflection and closure.
For ease of implementation, we divide and conquer, first to the Do List of the split:
- Into the compared with his test. (1) Funcvartostub must be a pointer variable to a function, and (2) the size of the return value list of the function must be equal to the length of the output.stubvals slice
- The Times variable in outputs is eliminated and converted into a pure list of multiple return values, that is, slices []values, set the slice variable to slice
- Constructs a closure function, the value of the free variable to i,i is [0, Len (slice)-1], and the list of return values for the closure function is slice[i]
- Replace the piling function with a closure function
Into the compared with his test
The code into compared with his references the implementation of the Stubfunc method, as follows:
funcPtrType := reflect.TypeOf(funcVarToStub)if funcPtrType.Kind() != reflect.Ptr || funcPtrType.Elem().Kind() != reflect.Func { panic("func variable to stub must be a pointer to a function")}funcType := funcPtrType.Elem()if funcType.NumOut() != len(outputs[0].StubVals) { panic(fmt.Sprintf("func type has %v return values, but only %v stub values provided", funcType.NumOut(), len(outputs[0].StubVals)))}
Construction Slice
The code to construct the slice is simple, as follows:
slice := make([]Values, 0)for _, output := range outputs { t := 0 if output.Times <= 1 { t = 1 } else { t = output.Times } for j := 0; j < t; j++ { slice = append(slice, output.StubVals) }}
Generate closures
The new encapsulated function getresultvalues is called in the code implementation that generated the closure, as follows:
i := 0len := len(slice)stubVal := reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value { if i < len { i++ return getResultValues(funcPtrType.Elem(), slice[i - 1]...) } panic("output seq is less than call seq!")})
The implementation of the new encapsulated function getresultvalues is referenced by the implementation of the Stubfunc method, as follows:
func getResultValues(funcType reflect.Type, results ...interface{}) []reflect.Value { var resultValues []reflect.Value for i, r := range results { var retValue reflect.Value if r == nil { retValue = reflect.Zero(funcType.Out(i)) } else { tempV := reflect.New(funcType.Out(i)) tempV.Elem().Set(reflect.ValueOf(r)) retValue = tempV.Elem() } resultValues = append(resultValues, retValue) } return resultValues}
Replace the piling function with closures
This is done directly by reusing the existing variable piling method stub, as follows:
return s.Stub(funcVarToStub, stubVal.Interface())
At this point, the Stubfuncseq method is finished, OH yeah!
Anti-pattern
From the previous article, "Gostub Framework Use Guide," the reader will write the following test code:
func TestFuncDemo(t *testing.T) { Convey("TestFuncDemo", t, func() { Convey("for succ", func() { var liLei = `{"name":"LiLei", "age":"21"}` stubs := StubFunc(&adapter.Marshal, []byte(liLei), nil) defer stubs.Reset() //several So assert }) Convey("for fail", func() { stubs := StubFunc(&adapter.Marshal, nil, ERR_ANY) //several So assert }) })}
Once the Gostub framework has a STUBFUNCSEQ interface, some readers will write the above test code in the following anti-pattern:
func TestFuncDemo(t *testing.T) { Convey("TestFuncDemo", t, func() { var liLei = `{"name":"LiLei", "age":"21"}` outputs := []Output{ Output{StubVals: Values{[]byte(liLei), nil}}, Output{StubVals: Values{ErrAny, nil}}, } stubs := StubFuncSeq(&adapter.Marshal, outputs) defer stubs.Reset() Convey("for succ", func() { //several So assert }) Convey("for fail", func() { //several So assert }) })}
Some readers may think that the above test code is better, but in general, a test function has multiple test cases, that is, the second-level convey number (5 or so is common). If the pile functions of all test cases are written together, it will be very complex, and many times it will exceed the limits of the human brain, so I call this model anti-pattern.
We encourage each use case to manage its own pile function, the separation of concerns.
Summary
In view of the complexity of the gostub framework, this paper has carried out two development of the framework, including the definition, use and implementation of the new interface Stubfuncseq, and the application of graceful change is not applicable, which improves the adaptability of the gostub framework. At the end of this paper, the anti-pattern of STUBFUNCSEQ interface is put forward, which makes the reader stay vigilant and use the gostub framework correctly.