Unit testing Go code with mocks and dependency injection

No False Positives

But Webfetch() also needs to handle error cases correctly. To check this, Listing 3 defines the Always404() handler, which tells the web server to send a 404 status code to the client for each request, plus a page of empty content. Quickly added to the new web server starting in line 15, Webfetch() now receives "Not Found" messages from the server and ensures this is actually happening in the if conditions starting at line 19 of Listing 3.

Listing 3

webfetch_404_test.go

01 package webfetcher
02
03 import (
04 "net/http"
05 "net/http/httptest"
06 "testing"
07 )
08
09 func Always404(w http.ResponseWriter,
10   r *http.Request) {
11   w.WriteHeader(http.StatusNotFound)
12 }
13
14 func TestWebfetch404(t *testing.T) {
15   srv := httptest.NewServer(
16     http.handlerFunc(Always404))
17   content, err := Webfetch(srv.URL)
18
19   if err == nil {
20     t.Errorf("No error on 404")
21   }
22
23   if len(content) != 0 {
24     t.Error("Content not empty on 404")
25   }
26 }

Independence

Alas, elegant inline servers with configurable handlers are not always available for all use cases. What do you do, for example, if a system needs a database? It is important to make sure that the dependency of the main program on the database is not hard-wired somewhere inside the system, but can be tinkered with from the outside. The procedure is known as "Dependency Injection" and feeds new objects with structures that define external targets while constructing the objects. This can lead to real mazes of dependencies with larger software architectures, which is why Uber [2] and Google [3] have already written packages to deal with it at scale.

Injection: This Isn't Going to Hurt

In order to avoid headaches for a library's end user, many developers would try to abstract as many details as possible in their software designs on first consideration. An example storage service for names, namestore, with a connected database as shown in Listing 4, would not initially reveal that it uses a database at all, but would simply create and manipulate the necessary scaffolding behind the drapes of the NewStore() constructor.

Listing 4

main-wrong.go

01 package main
02
03 import (
04   ns "namestore"
05 )
06
07 func main() {
08   nstore := ns.NewStore()
09   nstore.Insert("foo")
10 }

But this has fatal consequences for unit tests, which can no longer use the namestore package's interface for their tricks: For example, using a test-friendly SQLite database or even a driver for CSV format as the back end instead of a MySQL database that namestore might be using by default, which the test suite would then need to install and start in a complex process.

From a test-friendly design point of view, it makes more sense to have the library user pass the dependencies (such as the database used) to the constructor as shown in Listing 5, where the user opens the database (in this case SQLite) first and then passes the database handle to the namestore object's constructor, which then uses it for its inner workings.

Listing 5

main.go

01 package main
02
03 import (
04   "database/sql"
05   _ "github.com/mattn/go-sqlite3"
06   ns "namestore"
07 )
08
09 func main() {
10   db, err :=
11     sql.Open("sqlite3", "names.db")
12   if err != nil {
13     panic(err)
14   }
15
16   nstore := ns.NewStore(ns.Config{Db: db})
17   nstore.Insert("foo")
18 }

The implementation of a library that is unit test-friendly is shown in Listing 6. As a data container to pass the database handle and potentially other items to the library, line 8 defines the Config type structure, which the NewStore() constructor expects in line 12. The constructor then returns a pointer to it to the caller, and object methods like Insert() from line 16 can then be called by using them as "receivers," as in nstore.Insert() in line 17 of Listing 5. This way, Insert() in line 16 in Listing 6 gains access to the database connection previously defined by the user in config.Db.

Listing 6

namestore.go

01 package namestore
02
03 import (
04   "database/sql"
05   _ "github.com/mattn/go-sqlite3"
06 )
07
08 type Config struct {
09   Db *sql.DB
10 }
11
12 func NewStore(config Config) (*Config) {
13     return &config
14 }
15
16 func (config *Config) Insert(
17     name string) {
18   stmt, err := config.Db.Prepare(
19       "INSERT INTO names VALUES(?)")
20   if err != nil {
21     panic(err)
22   }
23
24   _, err = stmt.Exec(name)
25   if err != nil {
26     panic(err)
27   }
28
29   return
30 }

As you can see, the unit-test-friendly design ensures now that tests can be carried out by injecting different dependencies, either mocks or alternative databases, avoiding additional installation overhead, and making sure the tests are running super fast.

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.

  • PHP-CLI

    PHP is not just for websites. Command-line PHP scripting has been around for more than 10 years, which makes the language and its comprehensive libraries eminently suitable for the toolbox of any administrator who manages web servers.

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