Unit testing Go code with mocks and dependency injection

Programming Snapshot – Go Testing

© Lead Image © bowie15, 123RF.com

© Lead Image © bowie15, 123RF.com

Article from Issue 226/2019
Author(s):

Developers cannot avoid unit testing if they want their Go code to run reliably. Mike Schilli shows how to test even without an Internet or database connection, by mocking and injecting dependencies.

Not continuously testing code is no longer an option. If you don't test, you don't know if new features actually work – or if a change adds new bugs or even tears open old wounds again. While the Go compiler will complain about type errors faster than scripting languages usually do, and strict type checking rules out whole legions of careless mistakes from the outset, static checks can never guarantee that a program will run smoothly. To do that, you need a test suite that exposes the code to real-world conditions and sees whether it behaves as expected at run time.

Ideally, the test suite should run at lightning speed so that developers don't get tired of kicking it off over and over again. And it should be resilient, continuing to run even while the Internet connection on the bus ride to work occasionally drops. So, if the tests open a connection to a web server or need a running database, this is very much out of line with the idea of fast independent tests.

However, since hardly any serious software just keeps chugging along by itself without a surrounding infrastructure, it is important for the test suite to take care of any dependencies on external systems and replace them with Potemkin villages. These simulators (aka "mocks") slip into the role of genuine communication partners for the test suite, accepting its requests and returning programmed responses, just as their real world counterparts would.

What Can Go Wrong?

Listing 1 [1] shows a small library with the Webfetch() function, which expects a URL for test purposes and returns the content of the page hiding behind the URL. What could possibly go wrong when you're just fetching a website? First of all, the specified URL may not comply with the standardized format. Then there could be problems contacting the server: errors in DNS resolution, network time outs, or the server might just be taking a powernap. Or maybe the given URL does not refer to a valid document on the server, which then responds with a 404, or it requests a redirect with a 301, for example.

Listing 1

webfetch.go

01 package webfetcher
02
03 import (
04   "fmt"
05   "io/ioutil"
06   "net/http"
07 )
08
09 func Webfetch(url string) (string, error) {
10   resp, err := http.Get(url)
11
12   if err != nil {
13     return "", err
14   }
15
16   if resp.StatusCode != 200 {
17     return "", fmt.Errorf(
18           "Status: %d", resp.StatusCode)
19   }
20
21   defer resp.Body.Close()
22
23   body, err := ioutil.ReadAll(resp.Body)
24   if err != nil {
25     return "", fmt.Errorf(
26               "I/O Error: %s\n", err)
27   }
28   return string(body), nil
29 }

The code in Listing 1 checks for all of these potential errors and returns an error type if it finds one. Once the client has finally tapped into the stream of incoming bytes from the network as of line 23, it can happen that the stream is suddenly interrupted because the network connection breaks down. A good client should field all of these cases, and a good test suite should verify that the client does so in all situations.

No Fuss

Now the test-suite might run on systems that don't have a reliable Internet connection – nothing is more annoying than a program that sometimes works properly and sometimes doesn't. To avoid such dependencies, test suites often replace external systems with small-scale responders. In what is known as mocking, simple test frameworks mimic certain capabilities of external systems in a perfectly reproducible manner. For example, a simplified local web server is used that only delivers static pages or only reports error codes.

Programs in Go can even run a server in the same process as the test suite, thanks to its quasi-simultaneous Go routines. With no need to start an external process, this is incredibly convenient, because the whole time-consuming and error-prone fuss about starting and, above all, shutting down external processes properly even in erroneous conditions is no longer required.

Conventions

Listing 2 checks if the Webfetch() function from Listing 1 works and if the server delivers a text file as advertised. To do this, it defines the TestWebfetchOk() function as of line 18. The function expects a pointer to Go's standard testing data structure of the testing.T type as a parameter, which it later uses to report errors to the test suite.

Listing 2

webfetch_200_test.go

01 package webfetcher
02
03 import (
04   "fmt"
05   "net/http"
06   "net/http/httptest"
07   "testing"
08 )
09
10 const ContentString = "Hello, client."
11
12 func Always200(w http.ResponseWriter,
13                r *http.Request) {
14   w.WriteHeader(http.StatusOK)
15   fmt.Fprint(w, ContentString)
16 }
17
18 func TestWebfetchOk(t *testing.T) {
19   srv := httptest.NewServer(
20            http.HandlerFunc(Always200))
21   content, err := Webfetch(srv.URL)
22
23   if err != nil {
24     t.Errorf("Error on 200")
25   }
26
27   if content != ContentString {
28     t.Errorf("Expected %s but got %s",
29       ContentString, content)
30   }
31 }

It should be noted that Go insists on sticking to conventions and stubbornly ignores anything that deviates from them. The names of all test files must end with _test.go. So, it must be webfetch_200_test.go and not, say, webfetch_test_200.go, because otherwise the command go test would silently ignore the file and therefore wouldn't find any tests to execute. On top of that, the names of the test suite's test routines must start with func Test<tXXX>, otherwise Go won't recognize them as unit tests and won't run them at all.

And, finally, the "one package per directory" rule always applies in Go: All three Go programs, the webfetch.go library and the two files with the unit tests, all show the package webfetcher package at the beginning of their code.

The properly defined TestWebfetchOk() function in Listing 2 becomes part of the test suite, and the error checks in the if statements in lines 23 and 27 become its test cases. Line 23 verifies that the server sent an OK status code of 200, and line 27 compares the received string with the mock server's hard-coded string, specified in line 10 ("Hello, client."). In both cases, the test suite says nothing if everything runs smoothly and only reports any errors that may occur.

If you prefer your test routines to be a bit more talkative, you can use the t.Logf() function before each test case to display a message hinting at the test about to be performed; you can also generate some output in case of success. As implemented in Listing 2, the test suite called in verbose mode with

go test -v

does not give you any details on the individual test cases if everything passes, except that it reports the executed test functions (Figure 1). But if something were to go wrong, the error messages output with t.Errorf() would rear their ugly heads.

Figure 1: The test suite checks both success and failure conditions of the Webfetch library.

The Always200() handler in line 12 of Listing 2 defines the behavior of the built-in test web server. No matter what the incoming type *http.Request request looks like, it simply returns a status code of http.StatusOK (that is, 200) in the header of the HTTP response and adds the "Hello, client." string as the page content. Line 19 starts the actual web server from the httptest package, converts the previously defined handler to the http.HandlerFunc type, and hands it over to the server as a function.

The server's URL attribute specifies the host and port on which the new server is listening, which Line 21 hands over as a URL to the Webfetch() client function to be tested. From the client's point of view, a status code 200 and the previously set string are returned as expected, and the test suite does not raise an error.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Perl: Cucumber

    The Cucumber test framework helps developers and product departments jointly formulate test cases, not as program code, but in plain English. The initially skeptical Perlmeister has acquired a taste for this.

  • Go on the Rasp Pi

    We show you how to create a Go web app that controls Raspberry Pi I/O.

  • Perl: Regression Tests

    With a test suite, you can fix bugs and add new features without ruining the existing codebase.

  • Perl: Test-Driven Development

    Test-driven development with a full-coverage regression test suite as a useful side effect promises code with fewer errors. Mike "Perlmeister" Schilli enters the same path of agility and encounters a really useful new CPAN module.

  • Rat Rac

    If program parts running in parallel keep interfering with each other, you may have a race condition. Mike Schilli shows how to instruct the Go compiler to detect these conditions and how to avoid them in the first place.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News