Unit testing Go code with mocks and dependency injection
Programming Snapshot – Go Testing

© Lead Image © bowie15, 123RF.com
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.
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
(incl. VAT)
Buy Linux Magazine
Direct Download
Read full article as PDF:
Price $2.95
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Find SysAdmin Jobs
News
-
Kubuntu Focus Announces XE Gen 2 Linux Laptop
Another Kubuntu-based laptop has arrived to be your next ultra-portable powerhouse with a Linux heart.
-
MNT Seeks Financial Backing for New Seven-Inch Linux Laptop
MNT Pocket Reform is a tiny laptop that is modular, upgradable, recyclable, reusable, and ships with Debian Linux.
-
Ubuntu Flatpak Remix Adds Flatpak Support Preinstalled
If you're looking for a version of Ubuntu that includes Flatpak support out of the box, there's one clear option.
-
Gnome 44 Release Candidate Now Available
The Gnome 44 release candidate has officially arrived and adds a few changes into the mix.
-
Flathub Vying to Become the Standard Linux App Store
If the Flathub team has any say in the matter, their product will become the default tool for installing Linux apps in 2023.
-
Debian 12 to Ship with KDE Plasma 5.27
The Debian development team has shifted to the latest version of KDE for their testing branch.
-
Planet Computers Launches ARM-based Linux Desktop PCs
The firm that originally released a line of mobile keyboards has taken a different direction and has developed a new line of out-of-the-box mini Linux desktop computers.
-
Ubuntu No Longer Shipping with Flatpak
In a move that probably won’t come as a shock to many, Ubuntu and all of its official spins will no longer ship with Flatpak installed.
-
openSUSE Leap 15.5 Beta Now Available
The final version of the Leap 15 series of openSUSE is available for beta testing and offers only new software versions.
-
Linux Kernel 6.2 Released with New Hardware Support
Find out what's new in the most recent release from Linus Torvalds and the Linux kernel team.