Contributing
How to contribute to Rivaas
Thank you for your interest in contributing to Rivaas! We welcome contributions from everyone.
Ways to Contribute
You can help Rivaas in many ways:
- Report bugs — Tell us what’s broken
- Suggest features — Share your ideas
- Write documentation — Help others understand Rivaas
- Fix bugs — Submit pull requests
- Add features — Build new functionality
- Review code — Help us maintain quality
Getting Started
1. Find Something to Work On
Good first steps:
2. Set Up Your Environment
Fork and clone the repository:
git clone https://github.com/YOUR-USERNAME/rivaas.git
cd rivaas
Rivaas uses Nix for development. If you have Nix installed:
This gives you all the tools you need.
3. Make Your Changes
Create a new branch:
git checkout -b my-feature
Make your changes and test them:
# Run tests
go test ./...
# Run tests with race detection
go test -race ./...
# Check code style
golangci-lint run
4. Submit Your Work
Push your changes and create a pull request:
git push origin my-feature
Then open a pull request on GitHub.
Development Standards
We have clear standards for code quality. Please follow these guides:
Documentation Standards
Learn how to write good documentation for Go code.
Documentation Standards →
Testing Standards
Learn how to test your code properly.
Testing Standards →
Code Review Process
When you submit a pull request:
- Automated checks run — Tests, linting, and coverage checks
- Maintainer reviews — A maintainer looks at your code
- Feedback loop — You address any comments
- Approval — Maintainer approves when ready
- Merge — Your code becomes part of Rivaas!
Pull Request Guidelines
Good pull requests:
- Focus on one thing — Don’t mix unrelated changes
- Include tests — Test your changes
- Update documentation — Keep docs current
- Follow style guides — Match existing code style
- Write clear commit messages — Explain what and why
Commit messages:
Use clear, descriptive commit messages:
Add user authentication middleware
This adds JWT authentication middleware for protecting routes.
It validates tokens and adds user info to the context.
Fixes #123
Format:
- First line: Brief summary (under 72 characters)
- Blank line
- Detailed description (if needed)
- Reference issues with
Fixes #123 or Closes #456
Code of Conduct
We want Rivaas to be welcoming to everyone. Please:
- Be respectful — Treat others kindly
- Be constructive — Give helpful feedback
- Be patient — Everyone learns at different speeds
- Be inclusive — Welcome diverse perspectives
Questions?
Not sure about something? Ask!
License
By contributing to Rivaas, you agree that your contributions will be licensed under the Apache License 2.0.
Thank You!
Your contributions make Rivaas better for everyone. Thank you for helping!
1 - Documentation Standards
How to write clear documentation for Go code
This page explains how to write documentation for Rivaas code. Good documentation helps everyone understand how the code works.
Main Goal
Write clear documentation that explains:
- What the code does
- How to use it
- What inputs it needs and outputs it gives
What NOT to Write
Don’t mention these things in documentation:
Don’t use words like:
- “fast”, “slow”, “efficient”
- “optimized”, “quick”
- “high-performance”
- Any speed comparisons
Algorithm Details
Don’t include:
- Big-O notation (like O(1), O(n))
- Time or space complexity
- Algorithm names used to show speed
Benchmark Results
Don’t mention:
- “zero allocations”
- “optimized for speed”
- “50% faster”
- Any performance numbers
Memory Usage
Don’t talk about:
- “low memory usage”
- “minimal allocations”
- “memory-efficient”
- Specific memory amounts
Visual Decorations
Don’t use:
- Lines of equals signs or dashes
- ASCII art
- Empty comment lines for spacing
- Comments that add no information
Don’t write:
- “TODO: move this to…”
- “FIXME: this should be in…”
- “NOTE: consider moving to…”
Why not? If code needs to move, move it now. Don’t leave a comment about it. Use version control (git) to track changes.
Don’t write:
- “merged from…”
- “moved from…”
- “originally in…”
- “Benchmarks from X file”
Why not? Git tracks file history. Comments should explain what code does now, not where it came from.
What You SHOULD Write
Your documentation must focus on:
Purpose
- What the function, type, or method does
- Why it exists
- When to use it
Functionality
- What it does in simple words
- How it changes inputs to outputs
- Step-by-step behavior (when helpful)
Usage
- How to use it (with brief examples)
- Common use cases
- How to integrate it
Code Examples in Documentation
Public functions should include examples:
- Use tab-indented code blocks with
// Example: header - Show typical usage patterns
- Keep examples short and focused
- Use valid Go code that compiles
- Put examples after main description
Important: GoDoc needs tab indentation (not spaces) for code blocks.
Inline example format:
// FunctionName does something useful.
// It processes the input and returns a result.
//
// Example:
//
// result := FunctionName("input")
// fmt.Println(result)
//
// Parameters:
// - input: description
func FunctionName(input string) string { ... }
Runnable Example functions (preferred):
For public APIs, create Example functions in *_test.go files:
// In example_test.go
func ExampleFunctionName() {
result := FunctionName("input")
fmt.Println(result)
// Output: expected output
}
Parameters and Return Values
- What each parameter means
- What values are returned
- Error conditions and their meanings
Behavior and Edge Cases
- Expected behavior normally
- Edge cases and how they’re handled
- Side effects (if any)
- Thread safety (if relevant)
Constraints and Requirements
- Requirements to use it
- Limitations or known issues
- Dependencies
Error Documentation
Document when errors happen:
// Parse parses the input string into a Result.
// It returns an error if parsing fails.
//
// Errors:
// - [ErrInvalidFormat]: input string is malformed
// - [ErrEmpty]: input is an empty string
// - [ErrTooLong]: input exceeds maximum length
func Parse(input string) (Result, error) { ... }
Deprecation
Mark deprecated APIs clearly:
// Deprecated: Use [NewRouter] instead. This function will be removed in v2.0.
func OldRouter() *Router { ... }
// Deprecated: Use [Context.Value] with [RequestIDKey] instead.
func (c *Context) RequestID() string { ... }
Interface vs Implementation
Interfaces document the contract:
// Handler handles HTTP requests.
// Implementations must be safe for concurrent use.
// Handle should not modify the request after returning.
type Handler interface {
Handle(ctx *Context) error
}
Implementations reference the interface:
// JSONHandler implements [Handler] for JSON request/response handling.
// It automatically parses JSON request bodies and encodes JSON responses.
type JSONHandler struct { ... }
Generic Types
Document type parameter requirements:
// BindInto binds values from a ValueGetter into a struct of type T.
// T must be a struct type; using non-struct types results in an error.
// T should have exported fields with appropriate struct tags.
//
// Example:
//
// result, err := BindInto[UserRequest](getter, "query")
func BindInto[T any](getter ValueGetter, tag string) (T, error) { ... }
Thread Safety
Document concurrency behavior when relevant:
// Router is safe for concurrent use by multiple goroutines.
// Routes should be registered before calling [Router.ServeHTTP].
type Router struct { ... }
// Counter provides a thread-safe counter.
// All methods may be called concurrently from multiple goroutines.
type Counter struct { ... }
// Builder is NOT safe for concurrent use.
// Create separate Builder instances for each goroutine.
type Builder struct { ... }
Cross-References
Use bracket syntax [Symbol] to link to other symbols (Go 1.19+):
// Handle processes the request using the provided [Context].
// It returns a [Response] or an error.
// See [Router.Register] for how to register handlers.
func Handle(ctx *Context) (*Response, error) { ... }
Link targets:
[FunctionName] — links to function in same package[TypeName] — links to type in same package[TypeName.MethodName] — links to method[pkg.Symbol] — links to symbol in other package (e.g., [http.Handler])
Style Rules
GoDoc Standards
Clarity and Conciseness
- Use full sentences
- Keep comments short but meaningful
- Avoid unnecessary words
- Be direct and clear
Language Guidelines
- No marketing language — Avoid adjectives like:
- “simple”, “powerful”, “robust”, “amazing”
- “best”, “perfect”, “ideal”
- No superlatives — No “fastest” or “most reliable”
- Focus on facts — Describe what code does
Code Examples
- Public APIs need examples
- Use tab indentation (not spaces) for code blocks
- Prefer runnable Example functions in
*_test.go files - Keep examples minimal and focused
Package Documentation Files (doc.go)
When package documentation is long (more than a few lines), use a doc.go file:
- File name: Must be exactly
doc.go (lowercase) - Location: In the package root directory
- Content: Only package comment and package declaration
- Purpose: Keeps package overview separate from code
Format requirements:
- Start with
// Package [name] and clear description - First sentence is summary (shown in listings)
- Use markdown headers (
#) for sections - Include code examples when helpful
- Cover: purpose, main concepts, usage patterns
What to include:
- Package overview and purpose
- Key features
- Architecture (when relevant)
- Quick start examples
- Common usage patterns
- Links to examples or related packages
What NOT to include:
- Performance details
- Algorithm complexity
- File organization history
- Individual function documentation (put those in their files)
Example structure:
// Package router provides an HTTP router for Go.
//
// The router implements a routing system for cloud-native applications.
// It features path matching, parameter extraction, and comprehensive middleware support.
//
// # Key Features
//
// - Path matching for static and parameterized routes
// - Parameter extraction from URL paths
// - Context pooling for request handling
//
// # Quick Start
//
// package main
//
// import "rivaas.dev/router"
//
// func main() {
// r := router.New()
// r.GET("/", handler)
// r.Run(":8080")
// }
//
// # Examples
//
// See the examples directory for complete working examples.
package router
When to use doc.go:
- Use doc.go: Package documentation is long (multiple paragraphs, sections)
- Use inline comments: Package documentation is brief (1-3 sentences)
Examples
Good Documentation
// Register adds a new route to the [Router] using the given method and pattern.
// It returns the created [Route], which can be further configured.
// Register should be called during application setup before the server starts.
func (r *Router) Register(method, pattern string) *Route { ... }
// Context represents an HTTP request context.
// It provides access to the request, response writer, and route parameters.
// Context instances are pooled and reused across requests.
// Context is NOT safe for use after the handler returns.
type Context struct { ... }
// Param returns the value of the named route parameter.
// It returns an empty string if the parameter is not found.
// Parameters are extracted from the URL path during route matching.
//
// Example:
//
// userID := c.Param("id")
// fmt.Println(userID)
func (c *Context) Param(name string) string { ... }
Bad Documentation
// Register is a highly optimized router method with zero allocations.
// Uses O(1) lookup for fast routing.
// Extremely efficient performance characteristics.
func (r *Router) Register(method, pattern string) *Route { ... }
// Context is a fast, memory-efficient request context.
// Uses minimal allocations and provides high-performance access.
// Benchmarks show 50% faster than alternatives.
type Context struct { ... }
// Param returns the value with O(1) lookup time.
// Optimized for speed with zero allocations.
func (c *Context) Param(name string) string { ... }
// ========================================
// HTTP Context Methods
// ========================================
func (c *Context) Param(name string) string { ... }
// TODO: move this to a separate file
// Param returns the value of the named route parameter.
func (c *Context) Param(name string) string { ... }
Review Checklist
When writing or reviewing documentation, check:
Content Rules
Style Rules
Documentation Completeness
Examples and References
Special Cases
Additional Resources
Summary
Remember: Documentation explains what code does and how to use it, not how well it performs. Focus on functionality, behavior, and usage patterns. If performance is implied by code, don’t mention it in documentation.
2 - Testing Standards
How to write tests for Rivaas code
This page explains how to write tests for Rivaas. Good tests help us keep the code working correctly.
Test File Structure
All packages must have these test files:
*_test.go — Unit tests (same package)example_test.go — Examples for documentation (external package)*_bench_test.go — Performance benchmarks (same package)integration_test.go — Integration tests (external package)testing.go — Test helpers (if needed)
File Naming
| Test Type | File Name | Package |
|---|
| Unit tests | {package}_test.go | {package} |
| Benchmarks | {package}_bench_test.go | {package} |
| Examples | example_test.go | {package}_test |
| Integration | integration_test.go | {package}_test |
| Helpers | testing.go | {package} |
Test Naming
Use clear, descriptive names:
| Pattern | Use Case | Example |
|---|
TestFunctionName | Basic test | TestParseConfig |
TestFunctionName_Scenario | Specific scenario | TestParseConfig_EmptyInput |
TestFunctionName_ErrorCase | Error case | TestParseConfig_InvalidJSON |
TestType_MethodName | Method test | TestRouter_ServeHTTP |
Subtest Naming
For table-driven tests, use names that explain the scenario:
tests := []struct {
name string
// ...
}{
{name: "valid email address"}, // ✅ Good - descriptive
{name: "empty string returns error"}, // ✅ Good - explains behavior
{name: "test1"}, // ❌ Bad - not descriptive
{name: "case 1"}, // ❌ Bad - not helpful
}
Grouping with Subtests
Use nested t.Run() for related tests:
func TestUser(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
t.Run("valid input succeeds", func(t *testing.T) {
t.Parallel()
// test code
})
t.Run("invalid email returns error", func(t *testing.T) {
t.Parallel()
// test code
})
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
t.Run("existing user succeeds", func(t *testing.T) {
t.Parallel()
// test code
})
})
}
Package Organization
Unit Tests
- Package: Same as source (
package router) - Access: Can test public and internal APIs
- Use for: Testing individual functions, internal details, edge cases
- Framework: Standard
testing with testify/assert or testify/require
Integration Tests
- Package: External (
package router_test) - Access: Only public APIs (black-box testing)
- Use for: Testing full request/response cycles, component interactions
- Framework:
- Standard
testing for simple tests - Ginkgo/Gomega for complex scenarios
Example Tests
- Package: External (
package router_test) - Access: Only public APIs
- Use for: Showing how to use public APIs in documentation
Test Data Management
The testdata Directory
Go has special handling for testdata/ directories:
- Ignored by
go build - Used for test fixtures and sample data
- Accessible via relative path from tests
package/
├── handler.go
├── handler_test.go
└── testdata/
├── fixtures/
│ ├── valid_request.json
│ └── invalid_request.json
└── golden/
├── expected_output.json
└── expected_error.txt
Loading Test Data
func TestHandler(t *testing.T) {
t.Parallel()
// Load test fixture
input, err := os.ReadFile("testdata/fixtures/valid_request.json")
require.NoError(t, err)
// Use in test
result, err := ProcessRequest(input)
require.NoError(t, err)
// Compare with golden file
expected, err := os.ReadFile("testdata/golden/expected_output.json")
require.NoError(t, err)
assert.JSONEq(t, string(expected), string(result))
}
Golden File Testing
Golden files store expected output. Use -update flag to regenerate:
var updateGolden = flag.Bool("update", false, "update golden files")
func TestOutput_Golden(t *testing.T) {
result := GenerateOutput()
goldenPath := "testdata/golden/output.txt"
if *updateGolden {
err := os.WriteFile(goldenPath, []byte(result), 0644)
require.NoError(t, err)
return
}
expected, err := os.ReadFile(goldenPath)
require.NoError(t, err)
assert.Equal(t, string(expected), result)
}
Update golden files:
Assertions
Important: Always use assertion libraries. Don’t use manual if statements with t.Errorf().
testify/assert vs testify/require
assert: Continues test after failure (checks multiple things)require: Stops test after failure (when later checks depend on it)
// Use require when later code needs the value
result, err := FunctionThatShouldSucceed()
require.NoError(t, err) // Must succeed to continue
assert.Equal(t, expected, result)
// Use assert for independent checks
assert.NoError(t, err)
assert.Equal(t, expected, result)
assert.Contains(t, message, "success") // All run even if first fails
Error Checking
Always use testify error functions, not manual error checks.
Available Functions
assert.NoError(t, err) — Verify no error occurredassert.Error(t, err) — Verify an error occurredassert.ErrorIs(t, err, target) — Verify error wraps specific errorassert.ErrorAs(t, err, target) — Verify error is specific typeassert.ErrorContains(t, err, substring) — Verify error message contains text
When to Use Each
NoError / require.NoError:
result, err := FunctionThatShouldSucceed()
require.NoError(t, err) // Use require if result is needed
assert.Equal(t, expected, result)
Error / assert.Error:
_, err := FunctionThatShouldFail()
assert.Error(t, err) // Any error is fine
ErrorIs / assert.ErrorIs:
var ErrNotFound = errors.New("not found")
_, err := FunctionThatReturnsWrappedError()
assert.ErrorIs(t, err, ErrNotFound) // Check for specific error
ErrorAs / require.ErrorAs:
type ValidationError struct {
Field string
}
_, err := FunctionThatReturnsTypedError()
var validationErr *ValidationError
require.ErrorAs(t, err, &validationErr) // Use require if you need validationErr
assert.Equal(t, "email", validationErr.Field)
ErrorContains / assert.ErrorContains:
_, err := FunctionThatReturnsDescriptiveError()
assert.ErrorContains(t, err, "invalid input")
When to Use require vs assert for Errors
Use require when:
- Setup must succeed:
tmpfile, err := os.CreateTemp("", "test-*.txt")
require.NoError(t, err) // Must succeed to continue
defer os.Remove(tmpfile.Name())
- Need non-nil value:
db, err := sql.Open("postgres", dsn)
require.NoError(t, err) // Must succeed
require.NotNil(t, db) // Must not be nil
rows, err := db.Query("SELECT ...") // Safe to use db
- Later assertions depend on it:
err := c.Format(200, data)
require.NoError(t, err) // Must succeed for rest of test
// These assume Format succeeded
assert.Contains(t, w.Header().Get("Content-Type"), "application/xml")
assert.Contains(t, w.Body.String(), "<?xml")
Use assert when:
- Independent validations:
assert.NoError(t, err)
assert.Equal(t, expected, result)
assert.Contains(t, message, "success") // All checked even if first fails
- Non-critical checks:
err := optionalOperation()
assert.NoError(t, err) // Nice to have, but test can continue
assert.Equal(t, http.StatusOK, w.Code)
Table-Driven Tests
All tests with multiple cases should use table-driven pattern:
func TestFunctionName(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input any
want any
wantErr bool
}{
{
name: "valid input",
input: "test",
want: "result",
wantErr: false,
},
{
name: "invalid input",
input: "",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := FunctionName(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
Example Tests
All public APIs must have example tests in example_test.go:
package package_test
import (
"fmt"
"rivaas.dev/package"
)
// ExampleFunctionName demonstrates basic usage.
func ExampleFunctionName() {
result := package.FunctionName("input")
fmt.Println(result)
// Output: expected output
}
// ExampleFunctionName_withOptions demonstrates usage with options.
func ExampleFunctionName_withOptions() {
result := package.FunctionName("input",
package.WithOption("value"),
)
fmt.Println(result)
// Output: expected output
}
Example Guidelines
- Package must be
{package}_test - Function names start with
Example - Include
// Output: comments for deterministic examples - Use
log.Fatal(err) for error handling (acceptable in examples)
Benchmarks
Critical paths must have benchmarks in *_bench_test.go:
func BenchmarkFunctionName(b *testing.B) {
setup := prepareTestData()
b.ResetTimer()
b.ReportAllocs()
// Preferred: Go 1.23+ syntax
for b.Loop() {
FunctionName(setup)
}
}
func BenchmarkFunctionName_Parallel(b *testing.B) {
setup := prepareTestData()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FunctionName(setup)
}
})
}
Benchmark Guidelines
- Use
b.ResetTimer() after setup - Use
b.ReportAllocs() to track memory - Prefer
b.Loop() for Go 1.23+ - Test both sequential and parallel execution
- Use
b.Context() instead of context.Background() (Go 1.24+) - Use
b.Fatal(err) for setup failures (acceptable in benchmarks)
Integration Tests
Integration tests use the integration build tag:
//go:build integration
package package_test
import (
"net/http"
"net/http/httptest"
"testing"
"rivaas.dev/package"
)
func TestIntegration(t *testing.T) {
r := package.MustNew()
// Integration test code
}
| Test Type | Build Tag | Run Command |
|---|
| Unit tests | //go:build !integration | go test ./... |
| Integration tests | //go:build integration | go test -tags=integration ./... |
Why build tags?
- Tests excluded at compile time, not skipped at runtime
- Cleaner coverage reports
- Faster unit test runs
- Easy to run different suites in parallel
Ginkgo Integration Tests
For complex scenarios, use Ginkgo. Important: Only one RunSpecs call per package.
Suite file (one per package):
// {package}_integration_suite_test.go
//go:build integration
package package_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestPackageIntegration(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Package Integration Suite")
}
Test files (multiple allowed):
// integration_test.go
//go:build integration
package package_test
import (
"net/http"
"net/http/httptest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"rivaas.dev/package"
)
var _ = Describe("Feature Integration", func() {
var r *package.Router
BeforeEach(func() {
r = package.MustNew()
})
Describe("Scenario A", func() {
Context("with condition X", func() {
It("should behave correctly", func() {
req := httptest.NewRequest("GET", "/path", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusOK))
})
})
})
})
Using Labels for Filtering
Use labels to organize tests:
var _ = Describe("Router Stress Tests", Label("stress", "slow"), func() {
It("should handle high concurrent load", Label("stress"), func() {
// Stress test
})
})
Run with labels:
# Run only stress tests
ginkgo -label-filter=stress ./package
# Run everything except stress tests
ginkgo -label-filter='!stress' ./package
# Run tests with multiple labels (AND)
ginkgo -label-filter='integration && versioning' ./package
Test Helpers
Common utilities go in testing.go:
package package
import (
"testing"
"github.com/stretchr/testify/assert"
)
// testHelper creates a test instance with default configuration.
func testHelper(t *testing.T) *Config {
t.Helper()
return MustNew(WithTestDefaults())
}
// assertError checks if error matches expected.
func assertError(t *testing.T, err error, wantErr bool, msg string) {
t.Helper()
if wantErr {
assert.Error(t, err, msg)
} else {
assert.NoError(t, err, msg)
}
}
Always use t.Helper() in helper functions.
HTTP Testing Patterns
Testing Handlers
func TestHandler_GetUser(t *testing.T) {
t.Parallel()
handler := NewUserHandler(mockRepo)
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Header().Get("Content-Type"), "application/json")
var response User
err := json.NewDecoder(w.Body).Decode(&response)
require.NoError(t, err)
assert.Equal(t, "123", response.ID)
}
Testing with Request Body
func TestHandler_CreateUser(t *testing.T) {
t.Parallel()
body := strings.NewReader(`{"name": "Test User", "email": "test@example.com"}`)
req := httptest.NewRequest(http.MethodPost, "/users", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}
Testing Middleware
func TestAuthMiddleware(t *testing.T) {
t.Parallel()
tests := []struct {
name string
authHeader string
wantStatusCode int
}{
{
name: "valid token",
authHeader: "Bearer valid-token",
wantStatusCode: http.StatusOK,
},
{
name: "missing header",
authHeader: "",
wantStatusCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := AuthMiddleware(nextHandler)
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, tt.wantStatusCode, w.Code)
})
}
}
Context and Timeout Patterns
Testing with Context
func TestService_WithTimeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
t.Cleanup(cancel)
result, err := service.SlowOperation(ctx)
require.NoError(t, err)
assert.NotNil(t, result)
}
Using Test Context (Go 1.24+)
In Go 1.24+, use t.Context() instead of context.Background():
func TestWithContext(t *testing.T) {
t.Parallel()
// ✅ Preferred: Use t.Context()
ctx := t.Context()
// ❌ Avoid: context.Background()
// ctx := context.Background()
result, err := service.Operation(ctx)
require.NoError(t, err)
assert.NotNil(t, result)
}
Benefits: Automatically cancelled when test ends.
Mocking
Interface-Based Mocking (Preferred)
// Define interface
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
// Test implementation (fake)
type fakeUserRepository struct {
users map[string]*User
err error
}
func (f *fakeUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
if f.err != nil {
return nil, f.err
}
return f.users[id], nil
}
// Test using the fake
func TestUserService_GetUser(t *testing.T) {
t.Parallel()
repo := &fakeUserRepository{
users: map[string]*User{
"123": {ID: "123", Name: "Test User"},
},
}
service := NewUserService(repo)
user, err := service.GetUser(context.Background(), "123")
require.NoError(t, err)
assert.Equal(t, "Test User", user.Name)
}
HTTP Client Mocking
func TestAPIClient_FetchData(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/data", r.URL.Path)
assert.Equal(t, "Bearer token123", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id": "123", "name": "test"}`))
}))
t.Cleanup(server.Close)
client := NewAPIClient(server.URL, "token123")
data, err := client.FetchData(context.Background())
require.NoError(t, err)
assert.Equal(t, "123", data.ID)
}
Test Coverage
Requirements
| Package Type | Minimum | Target |
|---|
| Core packages | 80% | 90% |
| Utility packages | 75% | 85% |
| Integration packages | 70% | 80% |
Measuring Coverage
# Package coverage
go test -cover ./package
# Detailed report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Coverage by function
go tool cover -func=coverage.out
Best Practices
Parallel Execution: Use t.Parallel() for all tests (except testing.AllocsPerRun)
Assertions: Always use testify/assert or testify/require
Error Messages: Include descriptive messages
Test Isolation: Each test should be independent
Cleanup: Use t.Cleanup() instead of defer:
func TestWithResource(t *testing.T) {
t.Parallel()
resource := createResource()
t.Cleanup(func() {
resource.Close()
})
// Use resource...
}
Descriptive Names: Use clear test and subtest names
Documentation: Document complex test scenarios
Race Detection: Always run with -race in CI
Deterministic Tests: Avoid depending on:
- Current time (use clock injection)
- Random values (use fixed seeds)
- Network availability (use mocks)
- Filesystem state (use temp directories)
Running Tests
# Run unit tests (excludes integration)
go test ./...
# Run unit tests with verbose output
go test -v ./...
# Run unit tests with race detection (REQUIRED in CI)
go test -race ./...
# Run integration tests with race detection
go test -tags=integration -race ./...
# Run unit tests with coverage
go test -cover ./...
# Run benchmarks
go test -bench=. -benchmem ./...
# Run specific test by name
go test -run TestFunctionName ./...
# Run tests with timeout
go test -timeout 5m ./...
CI Commands
# Unit tests with race and coverage (CI)
go test -race -coverprofile=coverage.out -timeout 10m ./...
# Integration tests with race and coverage (CI)
go test -tags=integration -race -coverprofile=coverage-integration.out -timeout 10m ./...
Summary
Good tests:
- Use clear, descriptive names
- Use table-driven patterns for multiple cases
- Always use assertion libraries
- Run in parallel when possible
- Include examples for public APIs
- Test both success and error cases
- Use proper build tags for integration tests
- Have good coverage (80%+)
Remember: Tests are documentation too. Write them clearly!