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 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.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.