RETROSPECTIVE

October 19th, 2021

Creating a Go Module for Reusable Test Functions

Go

Kubernetes

Recently I wrote tests for my Kubernetes infrastructure in Go. These tests are split across multiple different repositories. However, there is a lot of overlap in testing logic between the test suites in each repository. In attempts to follow good programming practices and keep my code DRY, I split out the common code between the repositories into reusable functions. These functions exist in their own Go module, which is imported into the test suites as a dependency.

Go modules are part of Go's dependency management system1. They consist of a collection of packages, which are defied in a go.mod file. Go modules can be used as dependencies in other modules, as is the case with my reusable test function module and my Kubernetes test modules.

In this article, I first show how to configure a Go module, using my reusable test function module as an example. Then, I show how the reusable test function module is used in my Kubernetes test modules.

Go modules are defined in a go.mod file, similar to how a npm package is defined in a package.json file. go.mod exists in the root directory of a Go module. go.mod files have their own syntax separate from the Go programming language or any other configuration language2. The go.mod file for my reusable test function module has the following content:

module github.com/ajarombek/cloud-modules/kubernetes-test-functions go 1.15 require ( k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.3-beta.0 k8s.io/client-go v0.17.0 )

Go module files consist of multiple directives3. In the code above, module, go, and require are directives.

The module directive defines the path of the module, which is used as an identifier when it is imported into other modules. The path of my module is github.com/ajarombek/cloud-modules/kubernetes-test-functions. Notice that the module path is similar to the modules GitHub repository URL - github.com/AJarombek/cloud-modules/tree/master/kubernetes-test-functions. This is no coincidence; the module path needs to match the location where it is hosted4. Module paths with hosting locations allow Go to find the module when its path is used. In my module path, github.com is the hosting domain, ajarombek is the GitHub user, cloud-modules is the repository name, and kubernetes-test-functions is the directory within the repository containing the go.mod file.

The go directive specifies the version of Go that the module is written in. In my case, that version is 1.15, with the latest version of Go being 1.17 (as of October 2021).

The require directive specifies all the Go module dependencies and their minimum versions. My Go module has three dependency modules, all of which are Kubernetes modules.

Go modules can have additional directives, but these three are the most common ones you will see. The last thing needed to configure the Go module is to add tags to the GitHub repository. These tags specify different versions of the module. For example, one of my tags is kubernetes-test-functions/v0.2.10. This tag declares the version as v0.2.10, with kubernetes-test-functions specifying the directory containing the Go module.

With a Go module created and pushed to GitHub with tags specifying different versions, it is time to use the module within another module. In a different Go module, the require directive can be used to specify github.com/ajarombek/cloud-modules/kubernetes-test-functions as a dependency. The following code snippet is the go.mod file from one of my Go modules which tests the Kubernetes infrastructure for jarombek.com.

module github.com/ajarombek/jarombek-com-infrastructure/test-k8s go 1.14 require ( github.com/ajarombek/cloud-modules/kubernetes-test-functions v0.2.10 k8s.io/apimachinery v0.17.3-beta.0 k8s.io/client-go v0.17.0 )

The require directive specifies that version 0.2.10 (at a minimum) of the github.com/ajarombek/cloud-modules/kubernetes-test-functions module is a dependency of this Go module.

Now we can import and use the kubernetes-test-functions module. Go modules contain one or more packages, which are collections of source code files. In Go, the import statement consists of one or many package paths, which are strings. Using my jarombek.com Kubernetes tests as an example once again, the github.com/ajarombek/cloud-modules/kubernetes-test-functions package is imported and given an alias with the following code:

import ( k8sfuncs "github.com/ajarombek/cloud-modules/kubernetes-test-functions" ... )

The k8sfuncs package alias is used to invoke functions from the package, such as the following example:

func TestJarombekComDeploymentExists(t *testing.T) { k8sfuncs.DeploymentExists(t, ClientSet, "jarombek-com", namespace) }

To learn more about the Kubernetes test functions themselves, you can check out my previous article on writing Kubernetes tests with Go.

Two takeaways I had while writing Go modules were how easy they are to create and the elegance of the go.mod syntax. I love how simply naming Go modules with their host domain, in my case github.com, allows Go to resolve module dependencies. In my view, a great dependency management system can enhance a programming language and make it more viable for projects. Go modules make me even more likely to use the Go programming language in the future, as it continues to climb in my programming rankings. The code shown in this article is found in my cloud-modules and jarombek-com-infrastructure repositories.

[1] "Using Go Modules", https://go.dev/blog/using-go-modules

[2] "Go Modules Reference: go.mod files", https://golang.org/ref/mod#go-mod-file

[3] "Go Modules Reference: Grammar", https://golang.org/ref/mod#go-mod-file-grammar

[4] "Caveats if go package name doesn't start with github.com?", https://stackoverflow.com/a/49839325