- The
testing
package must be imported to provide testing functionality. - Generated mock files should not be placed in a
mocks
directory; instead, place them close to the interface. - Mock files should be included in code coverage.
-
The
github.com/stretchr/testify
library should be used to make tests clearer and more readable. It provides a comprehensive set of functions and is widely adopted in the Go community. -
The
github.com/stretchr/testify/mock
library should be used for creating mocks in tests, offering a flexible and easy-to-use API for mocking. -
reflect.DeepEqual
must not be used whenassert.Equal
orassert.EqualValues
are suitable alternatives. Thereflect.DeepEqual
function can have subtle nuances, potentially causing false positives and negatives in tests.// DO THIS assert.Equal(t, expected, actual) // NOT THIS if !reflect.DeepEqual(expected, actual) { t.Errorf("Expected and actual do not match.") }
-
If the test cannot proceed after an assertion failure, the
require
package should be used instead ofassert
.// DO THIS require.Equal(t, expected, actual) // NOT THIS assert.Equal(t, expected, actual)
The require
package stops test execution after a failing assertion, which is preferable when subsequent tests depend on previous assertions.
NOTE
This section is not complete, as the common library is not fully adopted.
When mocking dependencies for testing, the following guidelines should be observed:
- Mocks must be created using the
mockery
tool, a flexible library for creating and using mock objects in tests. - Mock objects should be used to isolate the code under test from its dependencies.
Example:
type DBMock struct {
mock.Mock
}
func (db *DBMock) FetchData(id string) (Data, error) {
args := db.Called(id)
return args.Get(0).(Data), args.Error(1)
}
func TestFetchData(t *testing.T) {
db := new(DBMock)
db.On("FetchData", "123").Return(Data{"123", "test data"}, nil)
res, err := db.FetchData("123")
db.AssertExpectations(t)
assert.NoError(t, err)
assert.Equal(t, Data{"123", "test data"}, res)
}
-
Each test function should test only a single function or method to promote clarity and ease of maintenance.
-
Table-driven tests should be used when testing multiple scenarios for the same function, improving readability and making it easier to add new cases.
-
Table-driven tests should not be used for complex functions that require extensive setup. In such cases, use subtests with
t.Run()
instead. -
For each table-driven test, use a struct for input and expected results.
tests := []struct { name string args args expected Type }{ // test cases here }
-
Test functions should not return any values; they should use the
Error
,Fail
, or related methods to signal failure.
- Test function names must be descriptive so that the purpose of a test is clear without reading the implementation.
- Error messages should provide clear context for failures, making diagnosis easier.
- If testing a specific method of a struct, the test function should follow the naming convention
TestStruct_Method
. For unexported structs, should useTest_struct_method
format.
- Mocks should be used when testing functions that depend on external services.
- Use interfaces to create decoupled code that is easier to test with mock implementations.
- Avoid using global variables as they can cause issues with maintaining state between tests. If needed, encapsulate global state within a struct and mock it in the test.
- You should include both positive and negative test cases.
- Test cases must cover all edge cases and boundary conditions. If a function behaves differently with an empty slice, zero value, or a specific boundary case, a test case must cover this scenario.
- Test cases should verify the correctness of a function rather than its performance. Performance tests can be written separately as benchmarks.
-
Tests should be able to run in any order and must not rely on the state from other tests or the order of execution.
-
Tests should be designed to run quickly, encouraging frequent test runs and faster feedback.
-
Tests should not rely on time checks, which can lead to flaky tests and false positives.
func TestCheckRateLimiterStatus(t *testing.T) { ... <-time.After(time.Second) // Bad practice doCheck() ... }
-
Tests must not rely on map keys or struct fields for order.
Following these guidelines will help ensure your Go tests are effective, maintainable, and easy to understand. Each rule should be considered in the context of your specific use case, with exceptions permissible when appropriate.
-
Should have a build tag "integration" to separate them from unit tests.
-
Should be placed in the same package as the code they’re testing.
-
To set up the environment for integration tests, should use a repeatable environment, such as Dockertest or Docker Compose.
-
The Testify suite should be used to organize integration tests, including setup and teardown steps to manage the lifecycle of Dockertest containers.
Here’s a simplified example:
type MyTestSuite struct { suite.Suite pool *dockertest.Pool resource *dockertest.Resource } // SetupTest runs before each test in the suite func (s *MyTestSuite) SetupTest() { var err error s.pool, err = dockertest.NewPool("") s.Require().NoError(err) s.resource, err = s.pool.Run("mysql", "5.7", []string{"MYSQL_ROOT_PASSWORD=secret"}) s.Require().NoError(err) // Allow time for the database to initialize time.Sleep(time.Second * 10) } // TearDownTest runs after each test in the suite func (s *MyTestSuite) TearDownTest() { if err := s.pool.Purge(s.resource); err != nil { log.Fatalf("Could not purge resource: %s", err) } } // An example test within the suite func (s *MyTestSuite) TestSomething() { // Test logic here } func TestMyTestSuite(t *testing.T) { suite.Run(t, new(MyTestSuite)) }