Here’s a pretty common problem, you want to write a piece of code whose job is to :
- call an external API
- get the response
- unmarshal it in the data structure you want
- return everything
Like this one : (source repo here)
package client
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
)
// HttpQuoteFetcher is the structure in charge of calling the API and get a response back
type HttpQuoteFetcher struct {
hostname string
}
// HttpQuoteFetcher constructor
func New(hostname string) *HttpQuoteFetcher {
return &HttpQuoteFetcher{hostname: hostname}
}
// Quote is the data structure we want back
type Quote struct {
Subject string `json:"subject"`
Author string `json:"author"`
Text string `json:"text"`
}
// FetchQuote is the method in charge of the whole process
func (fetcher HttpQuoteFetcher) FetchQuote(subject string) (*Quote, error) {
response, err := http.Get(fetcher.hostname + "/api/" + subject)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, errors.New(response.Status) // easy to test
}
respBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
quote := &Quote{}
if err := json.Unmarshal(respBody, "e); err != nil {
return nil, err
}
return quote, nil
}
Trouble always come with error handling
Using httptest
it’s quite easy to check an “happy case” & the error handling because of the
wrong response status code.
But the 3 other errors handling are much less obvious :
- failure of the http.Get
- failure of the ioutil.ReadAll
- failure of the json.Unmarshal
Let’s take the first one, http.Get :
We might be tempted to dig into the http
package in order to find a way to make it fail… wait !
In TDD, we want to ensure the way our production code behaves in an enumeration of logic flows (aka isolated tests).
To do so, we
- act on inputs & the context of execution, then
- check returns & side effects.
So what do we want here ?
The signature of the http.Get
function is cristal clear :
func Get(url string) (resp *Response, err error)
It takes a string
as input and returns an *http.Response
& an error
. We don’t need more !
But we’ve got 2 questions to answer :
- How our code should behave if the response is
nil
- How our code should behave if the error is NOT
nil
How the heck could that happen ? We just don’t care, it’s not our code. :) Let’s say we want to return an error in each case : we’ve got 2 tests to write.
Let’s abstract it
When testing pure business logic, we code everything against interfaces and it’s convenient to use in-memory fakes. But here, it’s very bound to our implementation so, even if it’s more fragile (aka it depends on our internal code), we don’t have much more choice.
Let’s refactor our code in a more abstract way :
package client
import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
)
type HttpQuoteFetcher struct {
hostname string
httpGet HttpGetter
readBody BodyReader
unMarshalResp RespUnmarshaller
}
type HttpGetter func(string) (*http.Response, error)
type BodyReader func(io.Reader) ([]byte, error)
type RespUnmarshaller func([]byte, interface{}) error
func New(hostname string) *HttpQuoteFetcher {
return &HttpQuoteFetcher{
hostname: hostname,
httpGet: http.Get,
readBody: ioutil.ReadAll,
unMarshalResp: json.Unmarshal,
}
}
type Quote struct {
Subject string `json:"subject"`
Author string `json:"author"`
Text string `json:"text"`
}
func (fetcher HttpQuoteFetcher) FetchQuote(subject string) (*Quote, error) {
response, err := fetcher.httpGet(fetcher.hostname + "/api/" + subject)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
return nil, errors.New(response.Status)
}
respBody, err := fetcher.readBody(response.Body)
if err != nil {
return nil, err
}
quote := &Quote{}
if err := fetcher.unMarshalResp(respBody, "e); err != nil {
return nil, err
}
return quote, nil
}
Our tests are still green, I’ll just clarify a few things :
- The 3 functions causing trouble in tests have seen their signatures abstracted out as
type
- These 3 new types are embedded in the data structure (unexported)
- The constructor simply plugs the old implementation for each of these properties
Variadic Constructor
Now, we want to alter the standard behavior in order to define what happens then.
We don’t want to alter the exposed API (so can keep calling New
with a simple string), we don’t want to expose
the HttpQuoteFetcher
because there’s just no point allowing to tweak them apart from the instanciation…
Let’s use the “Variadic constructor” pattern !
A “variadic function” simply means a function accepting a variable number of arguments. In golang you can do that with the last argument of a function that becomes a variable size array.
If we update our New()
signature to something like that :
func New(hostname string, mutators ...func(*HttpQuoteFetcher)) *HttpQuoteFetcher
We are now able to pass an arbitrary number of functions having all in common they have
*HttpQuoteFetcher
as receiver. The pointer is important since it will allow us to modify the structure on
each function application.
Here’s the new constructor :
func New(hostname string, mutators ...func(*HttpQuoteFetcher)) *HttpQuoteFetcher {
fetch := &HttpQuoteFetcher{
hostname: hostname,
httpGet: http.Get,
readBody: ioutil.ReadAll,
unMarshalResp: json.Unmarshal,
}
for _, mutator := range mutators {
mutator(fetch)
}
return fetch
}
and the function we can use in order to update the httpGet property :
func SetHttpGet(customHttpGet HttpGetter) func(*HttpQuoteFetcher) {
return func(fetcher *HttpQuoteFetcher) {
fetcher.httpGet = customHttpGet
}
}
Now, we can use it in our tests to trigger an error return for httpGet :
_, err := client.New(
"hostname",
client.SetHttpGet(func(string) (*http.Response, error) {
return nil, errors.New("anything here")
}),
).FetchQuote("subject")
assert.Error(t, err)
Conclusion
Since our new constructor still works if we pass no function (it will just behave “normally”), we don’t break the old API, no complexity leaks out of the package… and it’s pretty simple to test how our code behave in any case.
You can check the full repo here