Testing Pyramid and Integration Testing Practices for a Go Service
By applying Mike Cohn’s testing pyramid to a Go scheduling service, the author demonstrates a structured approach that combines straightforward unit tests, organized integration suites with setup/teardown hooks and coverage scripts, expressive GoConvey assertions, and end‑to‑end trace‑ID verification, while noting remaining gaps.
The author shares recent practice on testing a project that involves four services, with a core scheduling service, and discusses how to ensure output quality through a structured testing approach.
Testing Pyramid
According to Mike Cohn's "Testing Pyramid" concept, testing is divided into four layers:
Unit tests – test individual functions or classes.
Integration tests – test a service's interfaces.
End‑to‑end (link) tests – test the whole request flow across services.
UI tests – test functionality through the user interface.
Unit Testing
For a Go service, unit testing is straightforward: create a *_test.go file next to the source file and run go test . The primary metric is code coverage.
Integration Testing
Thoughts and Requirements
The goal is to automate integration tests for the HTTP APIs of the scheduling service, run all cases with a single command, and measure coverage (focused on the controller layer). The author adopts JUnit‑like concepts: TestCase (individual test) and TestSuite (a collection of related tests) with SetUp , Before , TearDown , and After hooks.
Implementation
A dedicated suites directory holds test suites. Each suite contains:
before.go – defines SetUp() and Before() .
after.go – defines TearDown() and After() .
run_test.go – the entry point for the suite.
package adapt
import "testing"
import . "github.com/smartystreets/goconvey/convey"
func TestRunSuite(t *testing.T) {
SetUp()
defer TearDown()
Convey("初始化", t, nil)
runCase(t, NormalCasePEE001)
runCase(t, PENormalCase01)
// ... other cases ...
runCase(t, NormalCase07)
runCase(t, NormalCase08)
runCase(t, NormalCasePIN003)
runCase(t, NormalCasePIN005)
runCase(t, NormalCasePIN006)
runCase(t, NormalCasePIN015)
}
func runCase(t *testing.T, testCase func(*testing.T)) {
Before()
defer After()
testCase(t)
}Test Case Writing
Test cases use the standard httptest package to send HTTP requests. No special handling is required.
Coverage
A shell script is used to generate coverage limited to the controller layer:
#!/bin/bash
go test -coverpkg xxx/controllers/... -coverprofile=report/coverage.out ./...
go tool cover -html=report/coverage.out -o report/coverage.html
open report/coverage.htmlIntroducing GoConvey
GoConvey provides a web UI, an editor for test cases, and live re‑run on file changes. It also offers expressive assertions:
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)
})
})
})
}The web UI shows failing cases and assertions clearly, and automatically re‑runs tests when source files change.
End‑to‑End Testing
End‑to‑end testing validates the full request flow across multiple services, possibly written in different languages. The author suggests using a trace ID passed via HTTP headers and a centralized log monitoring system to verify that each service logs the expected tags.
UI Testing
Currently performed manually by testers clicking through the UI, which is common in many companies.
Summary
The author reflects on several weeks of integration‑testing practice, acknowledges remaining gaps, and encourages adopting all four layers of the testing pyramid for complex, critical business logic.
Didi Tech
Official Didi technology account
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.