This page looks best with JavaScript enabled

Improving Code Quality with Testify in Go: A Deep Dive into Testing

 ·   ·  ☕ 13 min read

Introduction

Testify is a popular testing toolkit for the Go programming language. It provides a wide range of assertion functions, test suite support, and mocking capabilities, making it a powerful tool for testing Go applications. Testify aims to simplify the process of writing tests and improve the quality of test coverage in Go applications.

Testing is a critical aspect of software development that helps ensure that the software behaves as intended and catches bugs early in the development process. Writing tests can be time-consuming, but it is essential for delivering a reliable and maintainable software system. Testify can help make testing in Go more efficient and effective.

Testify offers a variety of benefits for Go developers:

  • Easy to use: Testify provides an intuitive and easy-to-use API for writing tests and assertions.
  • Wide range of assertions: Testify provides a large number of built-in assertions, which helps developers write thorough and comprehensive tests.
  • Test suite support: Testify supports test suites, which allows developers to group tests together for easier management.
  • Mocking capabilities: Testify provides a powerful mocking framework, making it easy to create and use mock objects in tests.
  • Integration with popular testing tools: Testify integrates with popular testing tools like GoConvey and Ginkgo, making it easy to incorporate into existing testing workflows.

Getting Started with Testify

Before using Testify, you need to install it. You can use the following command to install Testify using the Go module system:

go get github.com/stretchr/testify

To create a test file with Testify, you can create a new file with the “_test.go” suffix as we usually do in Go development, and import the “testing” and “github.com/stretchr/testify/assert” packages. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main_test

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestAddition(t *testing.T) {
    result := 2 + 2

    assert.Equal(t, 4, result, "The result should be 4")
}

In this example, we have created a test function named “TestAddition”. We are using the “assert” package from Testify to check if the result of the addition operation is equal to 4. But you will obviously be testing your business logic.

Testify provides a wide range of assertion functions that you can use in your test functions. Here are some examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func TestAssertions(t *testing.T) {
    assert.Equal(t, 1, 1, "1 should be equal to 1")
    assert.NotEqual(t, 1, 2, "1 should not be equal to 2")
    assert.Nil(t, nil, "Nil should be equal to nil")
    assert.NotNil(t, "test", "The string should not be nil")
    assert.True(t, true, "True should be true")
    assert.False(t, false, "False should be false")
    assert.Empty(t, "", "The string should be empty")
    assert.NotEmpty(t, "test", "The string should not be empty")
}

Testify Assertions

Testify provides a large number of assertion functions that you can use to check the expected behavior of your code in tests. These functions are simple to use and provide detailed error messages when assertions fail.

As we mentioned in previous section, here are some commonly used assertions provided by Testify:

  • assert.Equal: checks if two values are equal.
  • assert.NotEqual: checks if two values are not equal.
  • assert.Nil: checks if a value is nil.
  • assert.NotNil: checks if a value is not nil.
  • assert.True: checks if a value is true.
  • assert.False: checks if a value is false.
  • assert.Empty: checks if a value is empty.
  • assert.NotEmpty: checks if a value is not empty.

Refer to previous section on how to use them.

Testify also provides a mechanism for creating custom assertions if the built-in assertions are not enough. You can create a custom assertion by defining a function that takes a testing.TB interface, an expected value, and an actual value, and returns a boolean indicating whether the assertion passed or failed. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func assertGreaterThan(t testing.TB, expected, actual int) bool {
    if actual > expected {
        return true
    }
    return false
}

func TestCustomAssertions(t *testing.T) {
    assert.Condition(t, func() bool { return assertGreaterThan(t, 5, 10) }, "The value should be greater than 5")
}

In this example, we define a custom assertion function named “assertGreaterThan” that checks if the actual value is greater than the expected value. We then use this custom assertion function in a test function using Testify’s “assert.Condition” function.

Testify Mocking

Testify provides a mocking framework that allows you to create mock objects for testing. Mock objects are objects that simulate the behavior of real objects in a controlled way, making it easier to test your code in isolation. Testify’s mocking framework is based on Go interfaces and provides an easy-to-use API for creating and using mock objects.

Here is an example of creating and using a mock object with Testify:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type DB interface {
    Get(id string) (string, error)
    Put(id string, value string) error
}

type MockDB struct {
    mock.Mock
}

func (m *MockDB) Get(id string) (string, error) {
    args := m.Called(id)
    return args.String(0), args.Error(1)
}

func (m *MockDB) Put(id string, value string) error {
    args := m.Called(id, value)
    return args.Error(0)
}

func TestMocking(t *testing.T) {
    // Create a new instance of the mock DB
    mockDB := new(MockDB)

    // Define the expected behavior of the Get method
    mockDB.On("Get", "test-id").Return("test-value", nil)

    // Define the expected behavior of the Put method
    mockDB.On("Put", "test-id", "test-value").Return(nil)

    // Call the Get method
    value, err := mockDB.Get("test-id")
    assert.NoError(t, err)
    assert.Equal(t, "test-value", value)

    // Call the Put method
    err = mockDB.Put("test-id", "test-value")
    assert.NoError(t, err)

    // Check that the expected methods were called
    mockDB.AssertExpectations(t)
}

In this example, we define an interface for a database with two methods, Get and Put. We then define a mock implementation of this interface using Testify’s Mock type. The mock implementation overrides the Get and Put methods and uses Testify’s mock.Mock type to define the expected behavior of these methods.

In the TestMocking function, we create a new instance of the mock DB and define the expected behavior of the Get and Put methods using the On method. We then call these methods and check that they return the expected values. Finally, we use Testify’s AssertExpectations method to check that the expected methods were called.

Best practices for using Testify mocking

  1. Use mocking sparingly
    Mocking is a powerful technique for testing code in isolation, but it can also be overused. Mock objects should only be used when necessary, such as when testing code that has external dependencies or when testing code that is difficult to set up in a test environment.

  2. Mock only what is necessary
    When using mocking, it’s important to only mock what is necessary for the test. Over-mocking can lead to brittle tests that break easily when the implementation changes. Only mock the behavior that is required for the test to pass.

  3. Keep mocks simple
    Mocks should be simple and easy to understand. Avoid creating overly complex mock objects that are difficult to maintain or that obscure the intent of the test.

  4. Avoid using global mocks
    Global mock objects can make it difficult to understand the behavior of the test and can lead to unexpected behavior when running tests in parallel. Avoid using global mock objects and instead create new mock objects for each test.

  5. Use the AssertExpectations method
    Testify’s AssertExpectations method can be used to check that the expected methods were called on the mock object. Always use this method to ensure that the expected behavior was observed during the test.

  6. Use callback functions for complex mock behavior
    When defining complex mock behavior, use callback functions to define the behavior of the mock object. This can make it easier to understand the behavior of the mock and can simplify the code.

  7. Use AfterTest to clean up mocks
    Testify’s AfterTest method can be used to clean up any resources created during the test, such as mock objects. Always use this method to ensure that your tests are clean and don’t leak resources. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func TestMyTest(t *testing.T) {
    // Create a new instance of the mock object
    mockObj := new(MockObject)

    // Define the expected behavior of the mock object
    mockObj.On("Method", "arg").Return("result")

    // Call the method being tested
    result := myFunc(mockObj)

    // Verify the expected behavior of the mock object
    mockObj.AssertExpectations(t)

    // Clean up the mock object after the test
    t.Cleanup(func() {
        mockObj.AssertExpectations(t)
    })
}

In this example, we define a Cleanup function using Testify’s t.Cleanup method. This function ensures that the expected behavior of the mock object is verified again after the test is complete, using the AssertExpectations method. This ensures that the mock object is cleaned up properly after the test.

Testify Suites and Setup/Teardown

Testify provides a mechanism for grouping related tests into suites. Testify suites allow you to define setup and teardown functions that are called before and after the tests in the suite are run. This can be useful for setting up common test data or resources that are required for multiple tests in the suite.

Using setup and teardown functions

Testify allows you to define setup and teardown functions for each test, as well as for the suite as a whole. These functions can be used to set up test data, initialize resources, or perform any other necessary setup or teardown operations.

Here’s an example of defining setup and teardown functions for a single test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func TestMyTest(t *testing.T) {
    // Define the setup function
    setup := func() {
        // Set up test data or resources
    }

    // Define the teardown function
    teardown := func() {
        // Clean up test data or resources
    }

    // Call the test with setup and teardown functions
    t.Run("MyTest", func(t *testing.T) {
        setup()
        defer teardown()

        // Test code goes here
    })
}

In this example, we define a setup function that sets up test data or resources, and a teardown function that cleans up test data or resources. We then call the test using the t.Run method, which allows us to pass in the setup and teardown functions. The defer keyword is used to ensure that the teardown function is always called, even if the test fails.

Here’s an example of defining setup and teardown functions for a Testify suite:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func TestSuite(t *testing.T) {
    // Define the suite
    suite := testify.NewSuite()

    // Define the setup function for the suite
    suite.SetupSuite(func() {
        // Set up resources required by all tests in the suite
    })

    // Define the teardown function for the suite
    suite.TearDownSuite(func() {
        // Clean up resources used by all tests in the suite
    })

    // Add tests to the suite
    suite.Run(t, "MyTestSuite",
        testify.New(testMyFunc),
        testify.New(testAnotherFunc),
        // ...
    )
}

In this example, we define a Testify suite using the testify.NewSuite method. We then define the setup and teardown functions for the suite using the SetupSuite and TearDownSuite methods. Finally, we add tests to the suite using the Run method.

Examples of using suites and setup/teardown

Example 1: Testing an HTTP server

Suppose you have an HTTP server that you want to test. You can use a Testify suite to group related tests, and setup and teardown functions to start and stop the server before and after the tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func TestHTTPServer(t *testing.T) {
    // Define the suite
    suite := testify.NewSuite()

    // Define the setup function for the suite
    suite.SetupSuite(func() {
        // Start the server
        server.Start()
    })

    // Define the teardown function for the suite
    suite.TearDownSuite(func() {
        // Stop the server
        server.Stop()
    })

    // Add tests to the suite
    suite.Run(t, "HTTPServerTests",
        testify.New(testHTTPGet),
        testify.New(testHTTPPost),
        // ...
    )
}

func testHTTPGet(t *testing.T) {
    // Test the HTTP GET method
    // ...
}

func testHTTPPost(t *testing.T) {
    // Test the HTTP POST method
    // ...
}

In this example, we define a Testify suite to group tests related to an HTTP server. We use the SetupSuite function to start the server before the tests and the TearDownSuite function to stop the server after the tests. We then add tests to the suite using the Run function.

Example 2: Testing a database application

Suppose you have a database application that you want to test. You can use a Testify suite to group related tests, and setup and teardown functions to set up and tear down the database before and after the tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func TestDatabase(t *testing.T) {
    // Define the suite
    suite := testify.NewSuite()

    // Define the setup function for the suite
    suite.SetupSuite(func() {
        // Set up the database
        db.Setup()
    })

    // Define the teardown function for the suite
    suite.TearDownSuite(func() {
        // Tear down the database
        db.TearDown()
    })

    // Add tests to the suite
    suite.Run(t, "DatabaseTests",
        testify.New(testInsert),
        testify.New(testUpdate),
        // ...
    )
}

func testInsert(t *testing.T) {
    // Test inserting data into the database
    // ...
}

func testUpdate(t *testing.T) {
    // Test updating data in the database
    // ...
}

In this example, we define a Testify suite to group tests related to a database application. We use the SetupSuite function to set up the database before the tests and the TearDownSuite function to tear down the database after the tests. We then add tests to the suite using the Run function.

These are just a few examples of how Testify suites and setup/teardown functions can be used to organize and simplify your tests. By using these features, you can write more maintainable and robust tests for your Go applications.

Conclusion

In this blog post, we have covered the key features and benefits of Testify, a popular testing toolkit for Go. Testify provides a wide range of assertion functions and mocking capabilities that make it easier to write robust and maintainable tests for your Go applications. Some of the key benefits of using Testify include:

  • A rich set of assertion functions: Testify provides a large number of assertion functions that cover a wide range of use cases. These functions are easy to use and can help you write more comprehensive tests with less code.

  • Support for mocking: Testify provides a powerful mocking framework that makes it easier to test complex code that relies on external dependencies. By using Testify mocking, you can isolate your code from its dependencies and write more focused tests.

  • Easy to learn and use: Testify has a simple and intuitive API that makes it easy to get started with testing in Go. The toolkit is well-documented and there are many examples available online to help you learn how to use it effectively.

  • Integration with popular testing frameworks: Testify can be easily integrated with popular testing frameworks like Go’s built-in testing package, making it a versatile choice for testing Go applications.

If you’re interested in using Testify to test your Go applications, here are some next steps you can take:

  • Read the official Testify documentation: The official Testify documentation provides a comprehensive guide to using the toolkit, including detailed explanations of its features and API.

  • Explore the Testify examples: There are many examples of Testify in action available online, including on the official Testify GitHub repository. Reviewing these examples can help you understand how Testify can be used to test real-world Go applications.

  • Practice writing tests with Testify: The best way to learn how to use Testify is to practice writing tests with it. Start by writing some simple tests and gradually increase their complexity as you become more comfortable with the toolkit.

  • Contribute to the Testify project: If you find Testify useful and want to help improve it, consider contributing to the project on GitHub. You can contribute code, report bugs, or help improve the documentation to make Testify even more useful for the Go community.

By taking these steps, you can learn how to use Testify to write more effective tests for your Go applications and become a more skilled and effective Go developer.

Share on

Santosh Kumar
WRITTEN BY
Santosh Kumar
Santosh is a Software Developer currently working with NuNet as a Full Stack Developer.