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
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.