使具有多个依赖项的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)
    })
}