GoConvey

Welcome to GoConvey, a yummy testing tool for gophers.

Main Features

  • Integrates with go test
  • Readable, colorized console output
  • Fully-automatic web UI
  • Huge suite of regression tests
  • Test code generator

View a comprehensive table of all features compared to other Go testing tools.

Get going in 25 seconds

  1. In your terminal:
# make sure your GOPATH is set
 
cd <project path>
go get github.com/smartystreets/goconvey
go install github.com/smartystreets/goconvey
$GOPATH/bin/goconvey
  1. In your browser:
http://localhost:8080

If you have existing Go tests, they will run automatically and the results will appear in your browser.

Your first GoConvey test

Open any _test.go file and put this in it, customizing your package declaration:

package package_name
 
import (
	"testing"
	. "github.com/smartystreets/goconvey/convey"
)
 
func TestIntegerStuff(t *testing.T) {
	Convey("Given some integer with a starting value", t, func() {
		x := 1
 
		Convey("When the integer is incremented", func() {
			x++
 
			Convey("The value should be greater by one", func() {
				So(x, ShouldEqual, 2)
			})
		})
	})
}

Save the file, then glance over at your browser window, and you’ll see that the new tests have already been run.

Change the assertion (the line with So()) to make the test fail, then see the output change in your browser.

You can also run tests from the terminal as usual, with go test. If you want the tests to run automatically in the terminal, check out the auto-test script.

Required Reading

If I could ensure that every GoConvey user read only one bit of code from this repository it would be the isolated execution tests. Those tests are the very best documentation for the GoConvey execution model.

Full Documentation

See the documentation index for details about assertions, writing tests, execution, etc.

testify

Testify is being maintained at v1, no breaking changes will be accepted in this repo.

The @stretchr/testify library is a popular testing toolkit for the Go programming language. It provides a rich set of utilities to make writing tests more efficient and expressive. The library is widely used in the Go community due to its simplicity and powerful features. It extends the standard testing package in Go and offers additional functionalities such as assertions, mocking, and test suites.

  1. Assertions:

    • testify provides a comprehensive set of assertion functions that make it easy to validate conditions in your tests.
    • These assertions are more expressive and provide better error messages compared to the standard if checks in Go.
    • Common assertions include Equal, NotEqual, Nil, NotNil, True, False, Error, NoError, and many more.
package yours
 
import (
  "testing"
  "github.com/stretchr/testify/assert"
)
 
func TestSomething(t *testing.T) {
 
  // assert equality
  assert.Equal(t, 123, 123, "they should be equal")
 
  // assert inequality
  assert.NotEqual(t, 123, 456, "they should not be equal")
 
  // assert for nil (good for errors)
  assert.Nil(t, object)
 
  // assert for not nil (good when you expect something)
  if assert.NotNil(t, object) {
 
    // now we know that object isn't nil, we are safe to make
    // further assertions without causing any errors
    assert.Equal(t, "Something", object.Value)
 
  }
}
package mypackage
 
import (
    "testing"
 
    "github.com/stretchr/testify/assert"
)
 
func Add(a, b int) int {
    return a + b
}
 
func TestAdd(t *testing.T) {
    result := Add(2, 3)
 
    // Basic equality assertion
    assert.Equal(t, 5, result, "The sum should be 5")
 
    // Inequality assertion
    assert.NotEqual(t, 6, result, "The sum should not be 6")
 
    // More specific type assertion
    assert.IsType(t, 5, result, "Result should be an int")
 
    // Boolean assertions
    assert.True(t, result > 0, "Result should be positive")
    assert.False(t, result < 0, "Result should not be negative")
 
    // String assertions
    myString := "hello world"
    assert.Contains(t, myString, "world", "String should contain 'world'")
    assert.StartsWith(t, myString, "hello", "String should start with 'hello'")
 
    // Nil assertions
    var myNilPointer *int
    assert.Nil(t, myNilPointer, "Pointer should be nil")
 
    // Error assertions
    err := someFunctionThatMightError()
    assert.NoError(t, err, "Should be no error")
    // OR, if you expect an error:
    // assert.Error(t, err, "An error should have occurred")
    // assert.ErrorContains(t, err, "specific error message", "Error should contain message")
 
}
 
func someFunctionThatMightError() error {
    // Simulate an error (or no error)
    // return errors.New("something went wrong") // Uncomment to simulate error
    return nil
}
  1. Mocking:

    • The testify/mock package allows you to create mock objects for testing.
    • Mocking is useful when you want to isolate the unit under test by replacing dependencies with controlled implementations.
    • You can define expectations on mock objects, such as which methods should be called, with what arguments, and what they should return.
  2. Test Suites:

    • The testify/suite package provides a way to organize tests into suites.
    • Test suites are useful for grouping related tests and sharing common setup and teardown logic.
    • You can define setup (SetupTest) and teardown (TearDownTest) methods that run before and after each test in the suite.
package mypackage
 
import (
    "testing"
 
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
)
 
type MyTestSuite struct {
	suite.Suite
	// Add fields here for setup/teardown if needed
	// Example: db *sql.DB
}
 
func (s *MyTestSuite) SetupSuite() {
    // This runs once before all tests in the suite
    // Example: s.db = connectToDatabase()
    println("SetupSuite")
}
 
func (s *MyTestSuite) TearDownSuite() {
    // This runs once after all tests in the suite
    // Example: s.db.Close()
    println("TearDownSuite")
}
 
func (s *MyTestSuite) SetupTest() {
    // This runs before each test in the suite
    println("SetupTest")
}
 
func (s *MyTestSuite) TearDownTest() {
    // This runs after each test in the suite
    println("TearDownTest")
}
 
 
func (s *MyTestSuite) TestAdd() {
    result := Add(2, 3)
    assert.Equal(s.T(), 5, result, "The sum should be 5") // Use s.T() to get the *testing.T
}
 
func (s *MyTestSuite) TestMultiply() {
    result := 2 * 3
    assert.Equal(s.T(), 6, result, "The product should be 6")
}
 
func TestMySuite(t *testing.T) {
    suite.Run(t, new(MyTestSuite))
}

Note

Always use _test prefix on packages for writing tests but in case of internal tests in which you have access to private package members use _internal_test.go as filename.

Define the suite, and absorb the built-in basic suite functionality from testify - including a T() method which returns the current testing context

type ExampleTestSuite struct {
    suite.Suite
    VariableThatShouldStartAtFive int
}

Make sure that VariableThatShouldStartAtFive is set to five before each test

func (suite *ExampleTestSuite) SetupTest() {
    suite.VariableThatShouldStartAtFive = 5
}

All methods that begin with Test are run as tests within a suite.

func (suite *ExampleTestSuite) TestExample() {
    assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
    suite.Equal(5, suite.VariableThatShouldStartAtFive)
}

In order for go test to run this suite, we need to create a normal test function and pass our suite to suite.Run.

func TestExampleTestSuite(t *testing.T) {
    suite.Run(t, new(ExampleTestSuite))
}

SetupAllSuite has a SetupSuite method, which will run before the tests in the suite are run.

type SetupAllSuite interface {
 SetupSuite()
}

SetupTestSuite has a SetupTest method, which will run before each test in the suite.

type SetupTestSuite interface {
 SetupTest()
}

TearDownAllSuite has a TearDownSuite method, which will run after all the tests in the suite have been run.

type TearDownAllSuite interface {
 TearDownSuite()
}

TearDownTestSuite has a TearDownTest method, which will run after each test in the suite.

type TearDownTestSuite interface {
 TearDownTest()
}

BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input.

type BeforeTest interface {
 BeforeTest(suiteName, testName string)
}

AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input.

type AfterTest interface {
 AfterTest(suiteName, testName string)
}

WithStats implements HandleStats, a function that will be executed when a test suite is finished. The stats contain information about the execution of that suite and its tests.

type WithStats interface {
 HandleStats(suiteName string, stats *SuiteInformation)
}

Writing tests with testify is awesome, so use them. Also, I write tests with mock for the higher modules and tests the low level one with the real dependencies. For application that has really great mocks like redis, I have used them instead of real one.

Test Containers

Unit tests with real dependencies

Testcontainers is an open source library for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container.

No more need for mocks or complicated environment configurations. Define your test dependencies as code, then simply run your tests and containers will be created and then deleted.

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    ContainerRequest: testcontainers.ContainerRequest{
        Image:        "redis:5.0.3-alpine",
        ExposedPorts: []string{"6379/tcp"},
        WaitingFor:   wait.ForLog("Ready to accept connections"),
    },
    Started:          true,
})

What is Testcontainers?

Testcontainers is a library that provides easy and lightweight APIs for bootstrapping local development and test dependencies with real services wrapped in Docker containers. Using Testcontainers, you can write tests that depend on the same services you use in production without mocks or in-memory services.

Benefits of using Testcontainers

  • On-demand isolated infrastructure provisioning: You don’t need to have a pre-provisioned integration testing infrastructure. Testcontainers will provide the required services before running your tests. There will be no test data pollution, even when multiple build pipelines run in parallel because each pipeline runs with an isolated set of services.
  • Consistent experience on both local and CI environments: You can run your integration tests right from your IDE, just like you run unit tests. No need to push your changes and wait for the CI pipeline to complete.
  • Reliable test setup using wait strategies: Docker containers need to be started and fully initialized before using them in your tests. The Testcontainers library offers several out-of-the-box wait strategies implementations to make sure the containers (and the application within) are fully initialized. Testcontainers modules already implement the relevant wait strategies for a given technology, and you can always implement your own or create a composite strategy if needed.
  • Advanced networking capabilities: Testcontainers libraries map the container’s ports onto random ports available on the host machine so that your tests connect reliably to those services. You can even create a (Docker) network and connect multiple containers together so that they talk to each other via static Docker network aliases.
  • Automatic clean up: The Testcontainers library takes care of removing any created resources (containers, volumes, networks etc.) automatically after the test execution is complete by using the Ryuk sidecar container. While starting the required containers, Testcontainers attaches a set of labels to the created resources (containers, volumes, networks etc) and Ryuk automatically performs resource clean up by matching those labels. This works reliably even when the test process exits abnormally (e.g. sending a SIGKILL).

Testcontainers workflow

You can use Testcontainers with any testing library you are already familiar with. A typical Testcontainers-based integration test works as follows:

  • Before Test execution: Start your required services (databases, messaging systems etc.) as Docker containers using the Testcontainers API. Once the required containers start, configure or update your application configuration to use these containerized services and optionally initialize data needed for the test.
  • During Test execution: Your tests run using these containerized services.
  • After Test execution: Testcontainers takes care of destroying containers irrespective of whether tests are executed successfully or there were any failures.

GenericContainer abstraction

Testcontainers provides a programmatic abstraction called GenericContainer representing a Docker container. You can use GenericContainer to start a Docker container, get any container information such as hostname (the host under which the mapped ports are reachable), mapped ports, and stop the container.