Test Automation with Golang

Test Automation with Golang

Tailored example of an API test script for backend automation

ยท

5 min read

Golang has become a preferred choice for backend systems owing to its fast speed, straightforwardness, and dependability, and it is increasingly gaining prominence as a language of choice for writing backend test automation. Having had the chance to investigate this area, I would like to share my insights in this concise article, which will cover the process of writing API tests in Golang using very good tools like Ginkgo and Gomega.

Ginkgo is a BDD (Behavior-Driven Development) testing framework that allows developers to write tests in a natural language style. It makes it easy to write descriptive and organized tests. Gomega is a matcher library that can be used with Ginkgo to write more expressive and readable tests. Due to my prior experience with JavaScript and Python-based frameworks, I was naturally inclined towards the Ginkgo environment, as it's familiar and easy to grasp.

In this article, we will provide a sample test case for logging into the Rancher web page. To begin, ensure that you have Golang installed in your system. To download Ginkgo and Gomega libraries you can use the following commands:

go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...

Next, navigate to a directory where you want to create a test. Note that the directory name will be your package name for the test. Now you can create a new Ginkgo test suite by running the following command:

ginkgo bootstrap

This will create a sample test file as a template in the ./<package_name>/suite_test.go directory.

Let's break this down:

First, ginkgo bootstrap generates a new test file and places it in the <package name> package. Notice the name is with '*_test.go' in it.

Note: When testing Go code, unit tests for a package typically reside within the same directory as the package and are named *_test.go. Ginkgo follows this convention. It's also possible to construct test-only packages comprised solely of *_test.go files. refer ginkgo mental model to know more

You can either choose to write your test script directly in the '*_test.go' file or create a separate directory structure for improved maintenance. Personally, I prefer the latter approach, where I keep the test cases separate from the '*_test.go' runner file to enhance clarity and comprehension.

Here's an example of a runner file:

package go_rancher_suse_test

import (
    "testing"

    "github.com/onsi/ginkgo/v2"
    "github.com/onsi/gomega"
)

func TestGoRancherSuse(t *testing.T) {
    gomega.RegisterFailHandler(ginkgo.Fail)
    ginkgo.RunSpecs(t, "GoRancherSuse Suite")
}

Observe that the package name corresponds to the directory name, and the 'TestGoRancherSuse' function serves as a runner function with the 'T' parameter that will ultimately be passed on to the specs (i.e., test scripts) for making assertions.

Here's an example of a test script file:

package go_rancher_suse_test

import (
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"

    "github.com/onsi/ginkgo/v2"
    "github.com/onsi/gomega"
)

var _ = ginkgo.Describe("Given: Rancher API test", func() {
    ginkgo.Context("When: Login API executes with valid token", func() {
        url := "https://localhost/v3"
        //method := "POST"
        bearer_token := "Bearer token-l5gpk:g854bjdcfc4f27tpnb8gcvcxx6q59nwwpfwphgdlqvdcjvqzspttbc"

        ginkgo.It("Then: It should be able to login successfully", func() {

            // Perform Login and get response
            resp, err := ExecutePostRequest(url, bearer_token, nil)
            gomega.Expect(err).NotTo(gomega.HaveOccurred())

            // Check that the response status code is 200 OK
            log.Println("Validate that the response status code is 200 OK")
            gomega.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))

            defer resp.Body.Close()

            // Read the response body into a byte slice
            body, err := io.ReadAll(resp.Body)
            gomega.Expect(err).NotTo(gomega.HaveOccurred())

            // Parse the response body into a map
            var response map[string]interface{}
            err = json.Unmarshal(body, &response)
            gomega.Expect(err).NotTo(gomega.HaveOccurred())

            //keys := reflect.ValueOf(response).MapKeys()
            //fmt.Println("keys-->", keys)
            //fmt.Println("response-->", response)

            // Assert that the Rancher login was successful by checking for the presence of keys
            log.Println("Validate keys in the response")
            gomega.Expect(response).To(gomega.HaveKey("apiVersion"))
            gomega.Expect(response).To(gomega.HaveKey("baseType"))
            gomega.Expect(response).To(gomega.HaveKey("links"))

        })
    })
})

// Execute post request
func ExecutePostRequest(url string, token string, payload io.Reader) (*http.Response, error) {
    customTransport := http.DefaultTransport.(*http.Transport).Clone()
    customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
    client := &http.Client{Transport: customTransport}

    req, err := http.NewRequest(http.MethodPost, url, payload)

    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    req.Header.Add("Authorization", token)

    res, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    return res, nil
}

In this script, we use Ginkgo to define a test suite called "go_rancher_suse_test". We define one test case using the ginkgo. 'It' function that logs in to the Rancher web page.

To perform the login, we create an HTTP client using a bearer token for authentication. We send a POST request to the Rancher login endpoint, passing the token as the header. We expect the response status code to be 200 OK, and we read the response body into a byte slice. Assertion is further performed on the response using gomega.

We can continue creating test cases in the same directory in separate '.go' files which can be executed by a single runner file. it will be easier to control several execution hooks like before, after, before suite, after suite etc. this way.

Note: Folder structure is inspired from K8s. we have a seprate runner file to control hooks like before and after etc. Test cases can be written in seprate file like login_rancher.go.

Ginkgo has evolved significantly in recent years, empowering users to create tailored solutions using advanced features such as table-driven tests, results outputs, parallel tests and more. These features bring the tool closer to standard frameworks available in other programming languages, such as java, javascript and python.

To delve further into the subject, I recommend the following links:

ย