Golang basics - Writing Unit Tests (Part 3) Generating an HTML coverage report

Generating an HTML coverage report

If you use the following two commands you can visualise which parts of your program have been covered by the tests and which statements are lacking:

go test -cover -coverprofile=c.out  
go tool cover -html=c.out -o coverage.html

Then open coverage.html in a web-browser.

Go doesn't ship your tests

In addition, it may feel un-natural to leave files named addition_test.go in the middle of your package. Rest assured that the Go compiler and linker will not ship your test files in any binaries it produces.

Here is an example of finding the production vs test code in the net/http package we used in the previous Golang basics tutorial.

$ go list -f={{.GoFiles}} net/http
[client.go cookie.go doc.go filetransport.go fs.go h2_bundle.go header.go http.go jar.go method.go request.go response.go server.go sniff.go status.go transfer.go transport.go]

$ go list -f={{.TestGoFiles}} net/http
[cookie_test.go export_test.go filetransport_test.go header_test.go http_test.go proxy_test.go range_test.go readrequest_test.go requestwrite_test.go response_test.go responsewrite_test.go transfer_test.go transport_internal_test.go]

For more on the basics read the Golang testing docs.

1.4 Isolating dependencies


The key factor that defines a unit test is isolation from runtime-dependencies or collaborators.

This is achieved in Golang through interfaces, but if you're coming from a C# or Java background, they look a little different in Go. Interfaces are implied rather than enforced which means that concrete classes don't need to know about the interface ahead of time.

That means we can have very small interfaces such as io.ReadCloser which has only two methods made up of the Reader and Closer interfaces:

        Read(p []byte) (n int, err error)

Reader interface

        Close() error

Closer interface

If you are designing a package to be consumed by a third-party then it makes sense to design interfaces so that others can write unit tests to isolate your package when needed.

An interface can be substituted in a function call. So if we wanted to test this method, we'd just have to supply a fake / test-double class that implemented the Reader interface.

package main

import (
"fmt"
"io"
)

type FakeReader struct {
}

func (FakeReader) Read(p []byte) (n int, err error) {
// return an integer and error or nil
}

func ReadAllTheBytes(reader io.Reader) []byte {
// read from the reader..
}

func main() {
fakeReader := FakeReader{}
// You could create a method called SetFakeBytes which initialises canned data.
fakeReader.SetFakeBytes([]byte("when called, return this data"))
bytes := ReadAllTheBytes(fakeReader)
fmt.Printf("%d bytes read.\n", len(bytes))
}

Before implementing your own abstractions (as above) it is a good idea to search the Golang docs to see if there is already something you can use. In the case above we could also use the standard library in the bytes package:

    func NewReader(b []byte) *Reader

The Golang testing/iotest package provides some Reader implementations which are slow or which cause errors to be thrown half way through reading. These are ideal for resilience testing.


1.5 Worked example


I'm going to refactor the code example from the previous article where we found out how many astronauts were in space.

We'll start with the test file:

package main

import "testing"

type testWebRequest struct {
}

func (testWebRequest) FetchBytes(url string) []byte {
return []byte(`{"number": 2}`)
}

func TestGetAstronauts(t *testing.T) {
amount := GetAstronauts(testWebRequest{})
if amount != 1 {
t.Errorf("People in space, got: %d, want: %d.", amount, 1)
}
}

I have an exported method called GetAstronauts which calls into a HTTP endpoint, reads the bytes from the result and then parses this into a struct and returns the integer in the "number" property.

My fake / test-double in the test only returns the bare minimum of JSON needed to satisfy the test, and to begin with I had it return a different number so that I knew the test worked. It's hard to be sure whether a test that passes first time has worked.

Here's the application code where we run our main function. The GetAstronautsfunction takes an interface as its first argument allowing us to isolate and abstract away any HTTP logic from this file and its import list.

package main

import (
"encoding/json"
"fmt"
"log"
)

func GetAstronauts(getWebRequest GetWebRequest) int {
url := "http://api.open-notify.org/astros.json"
bodyBytes := getWebRequest.FetchBytes(url)
peopleResult := people{}
jsonErr := json.Unmarshal(bodyBytes, &peopleResult)
if jsonErr != nil {
log.Fatal(jsonErr)
}
return peopleResult.Number
}

func main() {
liveClient := LiveGetWebRequest{}
number := GetAstronauts(liveClient)

fmt.Println(number)
}

The GetWebRequest interface specifies the following function:

type GetWebRequest interface {  
FetchBytes(url string) []byte
}

Interfaces are inferred on rather than explicitly decorated onto a struct. This is different from languages like C# or Java.

The complete file named types.go looks like this and was extracted from the previous blog post:

package main

import (
"io/ioutil"
"log"
"net/http"
"time"
)

type people struct {
Number int `json:"number"`
}

type GetWebRequest interface {
FetchBytes(url string) []byte
}

type LiveGetWebRequest struct {
}

func (LiveGetWebRequest) FetchBytes(url string) []byte {
spaceClient := http.Client{
Timeout: time.Second * 2, // Maximum of 2 secs
}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Fatal(err)
}

req.Header.Set("User-Agent", "spacecount-tutorial")

res, getErr := spaceClient.Do(req)
if getErr != nil {
log.Fatal(getErr)
}

body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
log.Fatal(readErr)
}
return body
}

Choosing what to abstract

The above unit test is effectively only testing the json.Unmarshal function and our assumptions about what a valid HTTP response body would look like. This abstracting may be OK for our example, but our code coverage score will be low.

It is also possible to do lower level testing to make sure that the HTTP get timeout of 2 seconds is correctly enforced, or that we created a GET request instead of a POST.

Fortunately Go has set of helper functions for creating fake HTTP servers and clients.
Share On Facebook ! Tweet This ! Share On Google Plus ! Pin It ! Share On Tumblr ! Share On Reddit ! Share On Linkedin ! Share On StumbleUpon !

0 Response to " Golang basics - Writing Unit Tests (Part 3) Generating an HTML coverage report"

Posting Komentar