使具有多个依赖项的Golang业务方法可测试

时间:2018-09-17 12:57:38

标签: unit-testing go refactoring

我有一份单元测试员的工作,并且有一些功能是无法测试的。我试图告诉我的上司,他告诉我我无法重构代码以使其可测试。我将在今天的会议上提出这个问题,但是首先,我想确保我有一个可靠的计划来进行重构,以使业务用例不会发生变化。

方法

方法本身的定义如下:

//SendRequest This is used to contact the apiserver synchronously.
func (apiPath *APIPath) SendRequest(context interface{}, tokenHandler *apiToken.APITokenHandlerSt,
    header map[string]string,
    urlParams []string, urlQueries url.Values,
    jsonBody []byte) apiCore.CallResultSt {
    if apiToken := tokenHandler.GetToken(apiPath.AuthType, apiPath.Scope); apiToken != nil {
        return apiPath.APICoreHandler.SendRequest(
            context,
            apiToken.Token,
            apiPath.GetPath(urlParams, urlQueries), apiPath.Type,
            header, jsonBody)
    } else {
        errMsg, _ := json.Marshal(errors.InvalidAuthentication())
        return apiCore.CallResultSt{DetailObject: errMsg, IsSucceeded: false}
    }
}

定义接收者对象的位置:

//APIPath=======================
//Used for url construction
type APIPath struct {
    APICoreHandler *apiCore.APICoreSt
    // domain name of API
    DomainPath string
    ParentAPI  *APIPath
    Type       apiCore.APIType
    // subfunction name
    SubFunc          string
    KeyName          string
    AutoAddKeyToPath bool
    AuthType         oAuth2.OAuth2Type
    Scope            string
}

依赖项

您可能至少观察到其中两个:tokenHandler.GetTokenAPICoreHandler.SendRequest

这些的定义及其对象如下:

tokenHandler

type APITokenHandlerSt struct {
    Tokens []APITokenSt
}

tokenHandler.GetToken

// GetToken returns the token having the specified `tokenType` and `scope`
//
// Parameters:
// - `tokenType`
// - `scope`
//
// Returns:
// - pointer to Token having `tokenType`,`scope` or nil
func (ath *APITokenHandlerSt) GetToken(tokenType oAuth2.OAuth2Type, scope string) *APITokenSt {
    if ath == nil {
        return nil
    }
    if i := ath.FindToken(tokenType, scope); i == -1 {
        return nil
    } else {
        return &ath.Tokens[i]
    }
}

APICoreHandler

type APICoreSt struct {
    BaseURL string
}

APICoreHandler.SendRequest

//Establish the request to send to the server
func (a *APICoreSt) SendRequest(context interface{}, token string, apiURL string, callType APIType, header map[string]string, jsonBody []byte) CallResultSt {
    if header == nil {
        header = make(map[string]string)
    }
    if header["Authorization"] == "" {
        header["Authorization"] = "Bearer " + token
    }
    header["Scope"] = GeneralScope
    header["Content-Type"] = "application/json; charset=UTF-8"
    return a.CallServer(context, callType, apiURL, header, jsonBody)
}

APICoreHandler.CallServer

//CallServer Calls the server
//
// Parameters:
// - `context` : a context to pass to the server
// - `apiType` : the HTTP method (`GET`,`POST`,`PUT`,`DELETE`,...)
// - `apiURL` : the URL to hit
// - `header` : request header
// - `jsonBody`: the JSON body to send
//
// Returns:
// - a CallResultSt. This CallResultSt might have an error for its `DetailObject`
func (a *APICoreSt) CallServer(context interface{}, apiType APIType, apiURL string, header map[string]string, jsonBody []byte) CallResultSt {

    var (
        Url     = a.BaseURL + apiURL
        err     error
        res     *http.Response
        resBody json.RawMessage
        hc      = &http.Client{}
        req     = new(http.Request)
    )

    req, err = http.NewRequest(string(apiType), Url, bytes.NewBuffer(jsonBody))
    if err != nil {
        //Use a map instead of errorSt so that it doesn't create a heavy dependency.
        errorSt := map[string]string{
            "Code":    "ez020300007",
            "Message": "The request failed to be created.",
        }
        logger.Instance.LogError(err.Error())
        err, _ := json.Marshal(errorSt)
        return CallResultSt{DetailObject: err, IsSucceeded: false}
    }

    for k, v := range header {
        req.Header.Set(k, v)
    }

    res, err = hc.Do(req)
    if res != nil {
        resBody, err = ioutil.ReadAll(res.Body)
        res.Body = ioutil.NopCloser(bytes.NewBuffer(resBody))
    }
    return CallResultSt{resBody, logger.Instance.CheckAndHandleErr(context, res)}

}

我到目前为止的进展

很显然,tokenHandler并没有将业务作为对象传递,尤其是在不使用其状态时。因此,使该可测试性就像创建一个方法界面并使用它代替*apiToken.APITokenHandlerSt

一样简单。

但是,我关心的是该APICoreHandler及其SendRequest方法。我想知道如何重构它,以使被测代码的用例不发生变化,而让我来控制它。

这势在必行,因为我尚未测试的大多数方法都以某种方式命中了apiPath.SendRequest

更新:我进行了以下测试,这引起了恐慌:

func TestAPIPath_SendRequest(t *testing.T) {

    // create a fake server that returns a string
    fakeServer := httptest.NewServer(http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "Hello world!")
        }))
    defer fakeServer.Close()

    // define some values
    scope := "testing"
    authType := oAuth2.AtPassword

    // create a tokenHandler
    tokenHandler := new(apiToken.APITokenHandlerSt)
    tokenHandler.Tokens = []apiToken.APITokenSt{
        apiToken.APITokenSt{
            Scope:     scope,
            TokenType: authType,
            Token:     "dummyToken",
        },
    }

    // create some APIPaths
    validAPIPath := &APIPath{
        Scope:    scope,
        AuthType: authType,
    }

    type args struct {
        context      interface{}
        tokenHandler *apiToken.APITokenHandlerSt
        header       map[string]string
        urlParams    []string
        urlQueries   url.Values
        jsonBody     []byte
    }
    tests := []struct {
        name    string
        apiPath *APIPath
        args    args
        want    apiCore.CallResultSt
    }{}
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := tt.apiPath.SendRequest(tt.args.context, tt.args.tokenHandler, tt.args.header, tt.args.urlParams, tt.args.urlQueries, tt.args.jsonBody); !reflect.DeepEqual(got, tt.want) {
                t.Errorf("APIPath.SendRequest() = %v, want %v", got, tt.want)
            }
        })
    }

    t.Run("SanityTest", func(t *testing.T) {
        res := validAPIPath.SendRequest("context",
            tokenHandler,
            map[string]string{},
            []string{},
            url.Values{},
            []byte{},
        )
        assert.True(t,
            res.IsSucceeded)
    })
}

0 个答案:

没有答案