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.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
Canonical Releases Ubuntu 24.04
After a brief pause because of the XZ vulnerability, Ubuntu 24.04 is now available for install.
-
Linux Servers Targeted by Akira Ransomware
A group of bad actors who have already extorted $42 million have their sights set on the Linux platform.
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.
-
Kodi 21.0 Open-Source Entertainment Hub Released
After a year of development, the award-winning Kodi cross-platform, media center software is now available with many new additions and improvements.
-
Linux Usage Increases in Two Key Areas
If market share is your thing, you'll be happy to know that Linux is on the rise in two areas that, if they keep climbing, could have serious meaning for Linux's future.
-
Vulnerability Discovered in xz Libraries
An urgent alert for Fedora 40 has been posted and users should pay attention.
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.