duanou3868 2018-09-17 12:57
浏览 31

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

I have a job as a unit-tester, and there's a couple of functions that, as they are, are untestable. I have tried telling my immediate boss this, and he's telling me that I cannot refactor the code to make it testable. I will bring it up in today's meeting, but first, I want to make sure that I have a solid plan on doing the refactoring such that the business use case doesn't change.

The method

The method itself is defined like this:

//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}
    }
}

where its receiver object is defined thus:

//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
}

Dependencies

You may have observed at least two of them: tokenHandler.GetToken and APICoreHandler.SendRequest

The definitions of those, and their objects are as follows:

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)}

}

My progress thus far

Obviously, tokenHandler has no business being passed in as an object, especially when its state is not being used. Thus, making that testable would be as simple as create a one-method interface, and use it instead of the *apiToken.APITokenHandlerSt

My concern, however, is with that APICoreHandler and its SendRequest method. I would like to know how to refactor it such that the use case of this code under test doesn't change, whilst allowing me to control this.

This is imperative, because most of the methods I have yet to test, hit apiPath.SendRequest somehow

UPDATE: I made the following test attempt, which caused panic:

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条回答 默认 最新

    报告相同问题?

    悬赏问题

    • ¥100 任意维数的K均值聚类
    • ¥15 stamps做sbas-insar,时序沉降图怎么画
    • ¥15 unity第一人称射击小游戏,有demo,在原脚本的基础上进行修改以达到要求
    • ¥15 买了个传感器,根据商家发的代码和步骤使用但是代码报错了不会改,有没有人可以看看
    • ¥15 关于#Java#的问题,如何解决?
    • ¥15 加热介质是液体,换热器壳侧导热系数和总的导热系数怎么算
    • ¥100 嵌入式系统基于PIC16F882和热敏电阻的数字温度计
    • ¥15 cmd cl 0x000007b
    • ¥20 BAPI_PR_CHANGE how to add account assignment information for service line
    • ¥500 火焰左右视图、视差(基于双目相机)