(How I accidentally built a test naming system that worked)
It's common mantra in the Go ecosystem to test with the real thing whenever possible. But as our codebase grew, running tests with go test -v
became a reason to take a 5-minute coffee break. Here's how we solved it.
โก๏ธ Quick Go Testing Refresher
Go's testing conventions are beautifully simple:
-
Test files: End with
_test.go
(e.g.,service.go
โservice_test.go
) -
Test functions: Start with
TestXxx(t *testing.T)
- Run tests:
go test ./... # All tests in subdirectories
go test -v # Verbose output
go test -run TestFoo # Run specific tests
Now let's scale this foundation.
๐ Understanding Test Types in Go
As our system grew, we needed clear test categories:
-
Unit Tests
- Test isolated logic (pure functions, single components)
-
Example:
ValidateUserEmail()
formatting - Scope: Single package, no cross-boundary calls
- Speed: Instant (no I/O, only Go code)
-
Integration Tests
- Test interactions with real dependencies (DBs, APIs, Testcontainers)
- and cross-package boundaries
-
Example:
UserService.Save()
writing to PostgreSQL + callingauditlog
package - Speed: Slower (requires live services)
-
End-to-End (E2E) Tests
- Test full system workflows
- Example: HTTP API calls from auth to DB persistence or testing SDK-interfaces
- Speed: Very slow
With these categories defined, we needed a way to run them separately.
๐ง First Attempt: Environment Variables
if os.Getenv("INTEGRATION_TESTS") == "" {
t.Skip("Skipping integration test")
}
Problems emerged:
- IDE test runners would "pass" (but actually skip) tests
- CI configurations became bloated
- Easy to forget setting env vars
๐งช Second Try: Regex in Test Names
Next try postfixing:
func TestUserService_Integration(t *testing.T)
And running:
go test -run '/Integration$'
But this broke constantly in the monorepo with -C
directory changes.
๐ก The Solution: Go-Native Prefix Naming
Standardized on:
-
TestUnit_
for fast, isolated tests -
TestIntegration_
for cross-package/dependency tests
func TestUnit_User_ValidateEmail(t *testing.T) {}
func TestIntegration_UserService_CreateWithAuditLog(t *testing.T) {}
๐ Why This Works
- Blazing Fast Local Runs
go test -run ^TestUnit_ ./... # 0.3s vs 3 minutes
- Clear Intent
TestUnit_Parser_HandlesEdgeCases
TestIntegration_PaymentService_ProcessesRefund
- Simplified CI
jobs:
unit:
run: go test -run ^TestUnit_ ./...
integration:
run: go test -run ^TestIntegration_ ./...
- IDE Friendly Every Go-aware editor understands these prefix conventions.
๐ฆ Key Takeaways
-
Names Are Contracts -
TestUnit_
/TestIntegration_
creates clear boundaries - Fail Fast - Slow tests shouldn't sneak into quick runs
- Start Small - Just rename one test today
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.