Gostub Framework Two-time development practice

Source: Internet
Author: User
Tags assert
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:

    • Goconvey
    • Gostub
    • Gomock

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:

    1. 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

    2. 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.

    3. ...

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:

    1. Function interface
    2. 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:

    1. When the piling function is in a retry call scenario, there are multiple adjacent values in the outputs
    2. 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:

    1. 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
    2. 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
    3. 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]
    4. 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.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.