Unit Testing in Golang

📚 5 min read Tweet this post

When it comes to building robust and maintainable software, testing is crucial. And when we talk about testing in Go, unit testing comes first. Unit testing in Go is a powerful tool that helps developers write code that is more reliable, maintainable, and scalable. In this article, we’ll take a deep dive into unit testing in Go and its various features.

A unit test is a type of test that focuses on testing a single unit of code, such as a function or a method. The primary goal of unit testing is to ensure that each unit of code works as intended and is free of bugs. It is an essential practice that helps in reducing the number of bugs and errors in the code, making it more robust and easy to maintain. In Go, unit test including benchmark are written in a separate file with the _test.go suffix.

There are several benefits of unit testing, such as:

  1. Catching Bugs Early - Unit testing helps catch bugs early in the development process, making it easier and less expensive to fix them.
  2. Better Code Quality - Unit testing ensures that each unit of code works as intended, making the code more reliable and of better quality.
  3. Faster Development - With unit testing, developers can catch bugs and errors early, reducing the time it takes to develop software.
  4. Easy to Maintain - Unit testing helps in maintaining the codebase by catching bugs and errors before they become larger issues.

The basic unit test in Go is straightforward. Here is an example:

func TestAdd(t *testing.T) {
  sum := add(1, 2)
  if sum != 3 {
    t.Errorf("Sum was incorrect, got: %d, want: %d.", sum, 3)
  }
}

This test checks if the add function returns the correct result when given the input of 1 and 2. If it returns anything other than 3, the test will fail.

A table test is a type of unit test that allows developers to test multiple inputs and outputs in one test function. Here is an example:

func TestAddTable(t *testing.T) {
  tests := []struct {
    a int
    b int
    want int
  }{
    {1, 2, 3},
    {0, 0, 0},
    {10, -5, 5},
  }

  for _, tt := range tests {
    sum := add(tt.a, tt.b)
    if sum != tt.want {
      t.Errorf("Sum was incorrect, got: %d, want: %d.", sum, tt.want)
    }
  }
}

This test checks if the add function returns the correct result for multiple input-output pairs.

Benchmarking is a way to measure the performance of a function or method. Here is an example of a benchmark test in Go:

func BenchmarkAdd(b *testing.B) {
  for i := 0; i < b.N; i++ {
    add(1, 2)
  }
}

This benchmark test measures the performance of the add function by running it b.N times and recording the time it takes to complete each iteration. The results can be viewed by running go test -bench=. in the terminal.

Code coverage is a metric used to measure the percentage of code that is executed during testing. In Go, code coverage can be measured using the built-in go test command with the -cover flag. Here is an example:

go test -cover ./...

This command runs all the tests in the current directory and its subdirectories and displays the code coverage for each package.

A race condition occurs when two or more goroutines access the same shared resource concurrently, and at least one of them modifies it. This can lead to unexpected behavior and bugs in the code. Go provides a built-in race detector that can be enabled with the -race flag. Here is an example:

go test -race ./...

This command runs all the tests in the current directory and its subdirectories and checks for any race conditions.

Integration Test Integration tests are a type of test that tests the interaction between different parts of the system. Here is an example:

func TestIntegration(t *testing.T) {
  // Setup
  db := connectToDatabase()
  defer db.close()

  // Insert test data
  err := insertTestData(db)
  if err != nil {
    t.Fatalf("Failed to insert test data: %v", err)
  }

  // Run integration test
  result, err := queryDatabase(db)
  if err != nil {
    t.Fatalf("Failed to query database: %v", err)
  }

  // Check results
  expected := []int{1, 2, 3, 4, 5}
  if !reflect.DeepEqual(result, expected) {
    t.Errorf("Query result was incorrect, got: %v, want: %v.", result, expected)
  }
}

This integration test checks if the database query returns the expected result when given a specific input.

Mocking is a technique used in testing to simulate the behavior of external dependencies or services. In Go, mocking can be done using third-party libraries like gomock. Here are some examples:

ctrl := gomock.NewController(t)
defer ctrl.Finish()

client := mock_http.NewMockClient(ctrl)
client.EXPECT().Get("https://example.com").Return(&http.Response{StatusCode: 200}, nil)

result, err := fetchData(client, "https://example.com")
if err != nil {
  t.Fatalf("Failed to fetch data: %v", err)
}

// Check result
...

This HTTP mock test checks if the fetchData function returns the expected result when given a specific URL.

ctrl := gomock.NewController(t)
defer ctrl.Finish()

db := mock_sql.NewMockDB(ctrl)
db.EXPECT().Query("SELECT * FROM users WHERE id = ?", 1).Return(mock_sql.NewRows([]string{"id", "name"}).AddRow(1, "Alice"))

result, err := queryDatabase(db, 1)
if err != nil {
  t.Fatalf("Failed to query database: %v", err)
}

// Check result
...

This SQL mock test checks if the queryDatabase function returns the expected result when given a specific user ID. But I am not prefer using mock sql, just test SQL in integration test. You can use https://github.com/ory/dockertest to do that.

Go has a feature called build tags that allow you to include or exclude certain tests or code based on the build configuration. This can be useful when testing code that depends on platform-specific behavior or third-party dependencies that may not be available in certain environments.

Here is an example of using build tags to exclude a test on a specific platform:

// +build !windows

func TestFeature(t *testing.T) {
  // Test feature
}

This test will only be run on platforms that are not Windows.

You can also use build tags to run tests conditionally based on a directive. For example:

func TestFeature(t *testing.T) {
  if testing.Short() {
    t.Skip("Skipping test in short mode.")
  }

  // Test feature
}

This test will be skipped if the -short flag is passed to go test.

Unit testing is a crucial aspect of software development in Go. It helps ensure that your code works as expected and catches bugs early on in the development process. By implementing unit testing you can increase the reliability and maintainability of your code. Additionally, conditional unit testing can help you run tests conditionally based on certain build directives or configurations.

programming go unit test