Создание бизнес-метода Golang с несколькими зависимостями для тестирования - PullRequest
0 голосов
/ 17 сентября 2018

У меня есть работа в качестве юнит-тестера, и есть пара функций, которые, как они есть, не поддаются проверке.Я пытался сказать об этом своему непосредственному начальнику, и он говорит мне, что я не могу реорганизовать код, чтобы сделать его тестируемым.Я расскажу об этом на сегодняшнем собрании, но сначала я хочу убедиться, что у меня есть четкий план проведения рефакторинга, чтобы бизнес-сценарий не изменился.

Метод

Сам метод определяется следующим образом:

//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.GetToken и APICoreHandler.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)}

}

Мой прогресс таким образомfar

Очевидно, что 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)
    })
}
...