< up >
2024-01-23

go integration tests with ginkgo and gomega

Today, I’ve checked out the BDD1 testing framework ginkgo with the matcher library gomega for integration tests with go. The motivation was to find an alternative for my go-to testing framework rspec. Although in ruby, I anyway used it for go projects like blogctl.

rspec - Where I came from

The rspec DSL together with ruby itself makes it easy to write down tests pretty quickly. At least for me its quicker than doing the same in go (until now?):

require 'rspec'
require 'open3'

describe 'CLI' do
  context 'Trivial test cases' do
    it 'shows a version' do
      out, err, _ = Open3.capture3("blogctl --version")
      expect(err).to eq ""
      expect(out).to match(/BuildVersion:/)
      expect(out).to match(/BuildDate:/)
    end
  end
end

Integration test frameworks in go

There are a bunch of testing frameworks on the awesome-go list that look interesting for BDD integration testing.

Project Active development Specify tests within go Various nestable containers (Before|After)(All|Each) scripts
fulldump/biff y n ? ?
hedhyw/gherkingen y n ? ?
ginkgo y y y y
smartystreets/goconvey/ y y n ?
cucumber/godog y n y ?
corbym/gogiven y n ? ?
luontola/gospec n y ? ?
stesla/gospecify n ? ? ?
pavlo/gosuite n y n y

ginkgo

Ginkgo offers a similar Given-When-Then approach after looking at the example in the readme. Gomega is the matcher library, used by ginkgo. They’re both from the same author.

I’ve used my renaming tool r to check it out.

Setup

  1. get ginkgo cli: go install github.com/onsi/ginkgo/v2/ginkgo@latest
  2. add gomega dependency to your go-mod-managed project: go get github.com/onsi/gomega@latest
  3. initialize ginkgo which creates a testing entrypoint aka test suite: ginkgo bootstrap
  4. generate spec for main: ginkgo generate
  5. run tests: ginkgo

This should’ve created two files:

// test suite
// r_suite_test.go
package r_test

import (
  . "github.com/onsi/ginkgo/v2"
  . "github.com/onsi/gomega"
  "testing"
)

func TestBooks(t *testing.T) {
  RegisterFailHandler(Fail)
  RunSpecs(t, "R Suite")
}
// spec
// r_test.go
package r_test

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

var _ = Describe("R", func() {
})

Show version

At first I’ve adapted the version test from the previous rspec example:

package main_test

import (
	"os"
	"os/exec"
	"path/filepath"

	"github.com/bsm/gomega/gbytes"
	"github.com/bsm/gomega/gexec"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("R", func() {
	When("binary is used correctly", func() {
		It("shows version", func() {
			session, err := gexec.Start(
				exec.Command("ci-build/r", "--version"),
				GinkgoWriter,
				GinkgoWriter,
			)
			Expect(err).ShouldNot(HaveOccurred())
			Eventually(session).Should(gexec.Exit())
			Expect(session.Out).Should(gbytes.Say(`(?m)BuildVersion:.*\nBuildDate:`))
			Expect(session.Err).Should(gbytes.Say(""))
		})
	})
})

Real tests

The following tests use before and after scripts that set up the temporary environment for some real2 tests:

package main_test

import (
	"os"
	"os/exec"
	"path/filepath"

	"github.com/bsm/gomega/gbytes"
	"github.com/bsm/gomega/gexec"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("R", func() {
	When("binary is used correctly", Ordered, func() {
		var tempFile *os.File
		var tempDir string

		BeforeAll(func() {
			var err error
			tempDir, err = os.MkdirTemp(os.TempDir(), "")
			Expect(err).Should(BeNil())
		})

		AfterAll(func() {
			os.RemoveAll(tempDir)
		})

		BeforeEach(func(ctx SpecContext) {
			var err error
			tempFile, err = os.CreateTemp(tempDir, "")
			Expect(err).ShouldNot(HaveOccurred())
		})

		It("renames file", func() {
			renamed := filepath.Base(tempFile.Name()) + "2"
			session, err := gexec.Start(
				exec.Command("ci-build/r", tempFile.Name(), renamed),
				GinkgoWriter,
				GinkgoWriter,
			)
			Expect(err).ShouldNot(HaveOccurred())
			Eventually(session).Should(gexec.Exit())

			_, err = os.Stat(tempFile.Name())
			Expect(err).To(MatchError(os.IsNotExist, "It is not existing"))

			_, err = os.Stat(filepath.Join(tempDir, renamed))
			Expect(err).Should(BeNil())
		})

		It("shows version", func() {
			session, err := gexec.Start(
				exec.Command("ci-build/r", "--version"),
				GinkgoWriter,
				GinkgoWriter,
			)
			Expect(err).ShouldNot(HaveOccurred())
			Eventually(session).Should(gexec.Exit())
			Expect(session.Out).Should(gbytes.Say(`(?m)BuildVersion:.*\nBuildDate:`))
			Expect(session.Err).Should(gbytes.Say(""))
		})
	})

	When("binary is used incorrectly", func() {
		It("fails without any argument", func() {
			command := exec.Command("ci-build/r")
			session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
			Expect(err).ShouldNot(HaveOccurred())
			Eventually(session).Should(gexec.Exit())
			Expect(session.Out).Should(gbytes.Say("usage: r <path> <new_filename>"))
			Expect(session.Err).Should(gbytes.Say(""))
		})
	})
})

Thoughts

The tested framework feels much more like rspec than the standard library testing framework. It looks promising for BDD-alike tests in go. I’ll keep an eye on that when implementing integration tests for my next go projects.


  1. Behavior-Driven-Development having Given-When-Then-alike structures.
  2. Real as in it modifies the system environment by creating and removing files