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