So you want to reach 100% coverage ?

in your "impure" Go code

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, &quote); 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 :

  1. failure of the http.Get
  2. failure of the ioutil.ReadAll
  3. 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 :

  1. How our code should behave if the response is nil
  2. 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, &quote); 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

 
comments powered by Disqus