Documentation Index
Fetch the complete documentation index at: https://mintlify.com/go-kratos/kratos/llms.txt
Use this file to discover all available pages before exploring further.
This guide covers testing strategies for Kratos microservices, including unit tests, integration tests, and end-to-end tests.
Testing Overview
Kratos services should be tested at multiple levels:
- Unit Tests: Test individual functions and business logic
- Integration Tests: Test data layer and external dependencies
- Service Tests: Test service layer and API contracts
- End-to-End Tests: Test complete request flows
Unit Testing
Test your business logic layer independently:
package biz
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock repository
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) CreateUser(ctx context.Context, user *User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepo) GetUser(ctx context.Context, id string) (*User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestUserUseCase_CreateUser(t *testing.T) {
mockRepo := new(MockUserRepo)
useCase := NewUserUseCase(mockRepo, nil)
tests := []struct {
name string
user *User
mockFn func()
wantErr bool
}{
{
name: "successful creation",
user: &User{
Name: "John Doe",
Email: "john@example.com",
},
mockFn: func() {
mockRepo.On("CreateUser", mock.Anything, mock.AnythingOfType("*biz.User")).Return(nil)
},
wantErr: false,
},
{
name: "duplicate email",
user: &User{
Name: "Jane Doe",
Email: "john@example.com",
},
mockFn: func() {
mockRepo.On("CreateUser", mock.Anything, mock.AnythingOfType("*biz.User")).Return(ErrUserExists)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo.ExpectedCalls = nil
tt.mockFn()
err := useCase.CreateUser(context.Background(), tt.user)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockRepo.AssertExpectations(t)
})
}
}
Use table-driven tests for comprehensive coverage:
package biz
import "testing"
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"missing domain", "user@", false},
{"empty string", "", false},
{"with subdomain", "user@mail.example.com", true},
{"special characters", "user+tag@example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidateEmail(tt.email); got != tt.want {
t.Errorf("ValidateEmail() = %v, want %v", got, tt.want)
}
})
}
}
Integration Testing
Testing Data Layer
Test your data layer with a test database:
internal/data/user_test.go
package data
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
// Auto migrate tables
err = db.AutoMigrate(&User{})
if err != nil {
t.Fatalf("failed to migrate: %v", err)
}
return db
}
func TestUserRepo_Create(t *testing.T) {
db := setupTestDB(t)
repo := NewUserRepo(db, nil)
user := &User{
Name: "Test User",
Email: "test@example.com",
}
err := repo.CreateUser(context.Background(), user)
assert.NoError(t, err)
assert.NotEmpty(t, user.ID)
// Verify in database
var found User
err = db.Where("email = ?", user.Email).First(&found).Error
assert.NoError(t, err)
assert.Equal(t, user.Name, found.Name)
}
func TestUserRepo_GetByID(t *testing.T) {
db := setupTestDB(t)
repo := NewUserRepo(db, nil)
// Create test user
user := &User{
Name: "Test User",
Email: "test@example.com",
}
db.Create(user)
// Test retrieval
found, err := repo.GetUser(context.Background(), user.ID)
assert.NoError(t, err)
assert.Equal(t, user.ID, found.ID)
assert.Equal(t, user.Email, found.Email)
}
Using Test Containers
Use real databases with testcontainers:
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupPostgresContainer(t *testing.T) (string, func()) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "password",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("failed to start container: %v", err)
}
host, _ := container.Host(ctx)
port, _ := container.MappedPort(ctx, "5432")
dsn := fmt.Sprintf("host=%s port=%s user=postgres password=password dbname=testdb sslmode=disable",
host, port.Port())
cleanup := func() {
container.Terminate(ctx)
}
return dsn, cleanup
}
Service Testing
Testing Service Layer
Test your service implementation:
internal/service/user_test.go
package service
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
v1 "yourproject/api/user/v1"
)
type MockUserUseCase struct {
mock.Mock
}
func (m *MockUserUseCase) CreateUser(ctx context.Context, user *biz.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func TestUserService_CreateUser(t *testing.T) {
mockUC := new(MockUserUseCase)
svc := NewUserService(mockUC)
req := &v1.CreateUserRequest{
Name: "John Doe",
Email: "john@example.com",
}
mockUC.On("CreateUser", mock.Anything, mock.MatchedBy(func(u *biz.User) bool {
return u.Name == req.Name && u.Email == req.Email
})).Return(nil)
resp, err := svc.CreateUser(context.Background(), req)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.Id)
mockUC.AssertExpectations(t)
}
Testing HTTP Handlers
Test HTTP endpoints directly:
internal/server/http_test.go
package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHTTPServer_CreateUser(t *testing.T) {
// Setup
mockService := new(MockUserService)
server := NewHTTPServer(mockService)
// Prepare request
body := map[string]string{
"name": "John Doe",
"email": "john@example.com",
}
jsonBody, _ := json.Marshal(body)
req := httptest.NewRequest("POST", "/v1/users", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
// Mock expectations
mockService.On("CreateUser", mock.Anything, mock.Anything).Return(&v1.User{
Id: "123",
Name: body["name"],
Email: body["email"],
}, nil)
// Execute
server.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
var resp v1.User
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "123", resp.Id)
assert.Equal(t, body["name"], resp.Name)
}
gRPC Testing
Testing gRPC Services
Test gRPC services with bufconn:
internal/server/grpc_test.go
package server
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
v1 "yourproject/api/user/v1"
)
const bufSize = 1024 * 1024
var lis *bufconn.Listener
func setupGRPCServer(t *testing.T) (*grpc.Server, func(context.Context, string) (net.Conn, error)) {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
// Register your service
userService := NewUserService(nil)
v1.RegisterUserServiceServer(s, userService)
go func() {
if err := s.Serve(lis); err != nil {
t.Logf("Server exited with error: %v", err)
}
}()
bufDialer := func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
return s, bufDialer
}
func TestGRPCServer_CreateUser(t *testing.T) {
s, bufDialer := setupGRPCServer(t)
defer s.Stop()
ctx := context.Background()
conn, err := grpc.DialContext(ctx, "bufnet",
grpc.WithContextDialer(bufDialer),
grpc.WithInsecure(),
)
assert.NoError(t, err)
defer conn.Close()
client := v1.NewUserServiceClient(conn)
resp, err := client.CreateUser(ctx, &v1.CreateUserRequest{
Name: "John Doe",
Email: "john@example.com",
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.Id)
}
End-to-End Testing
Full Integration Tests
Test complete request flows:
package e2e
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
v1 "yourproject/api/user/v1"
)
type E2ETestSuite struct {
suite.Suite
client v1.UserServiceClient
conn *grpc.ClientConn
}
func (s *E2ETestSuite) SetupSuite() {
// Connect to running service
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, "localhost:9000", grpc.WithInsecure())
s.Require().NoError(err)
s.conn = conn
s.client = v1.NewUserServiceClient(conn)
}
func (s *E2ETestSuite) TearDownSuite() {
s.conn.Close()
}
func (s *E2ETestSuite) TestUserLifecycle() {
ctx := context.Background()
// Create user
createResp, err := s.client.CreateUser(ctx, &v1.CreateUserRequest{
Name: "E2E Test User",
Email: "e2e@example.com",
})
s.NoError(err)
s.NotEmpty(createResp.Id)
// Get user
getResp, err := s.client.GetUser(ctx, &v1.GetUserRequest{
Id: createResp.Id,
})
s.NoError(err)
s.Equal(createResp.Id, getResp.Id)
s.Equal("E2E Test User", getResp.Name)
// Update user
updateResp, err := s.client.UpdateUser(ctx, &v1.UpdateUserRequest{
Id: createResp.Id,
Name: "Updated Name",
})
s.NoError(err)
s.Equal("Updated Name", updateResp.Name)
// Delete user
_, err = s.client.DeleteUser(ctx, &v1.DeleteUserRequest{
Id: createResp.Id,
})
s.NoError(err)
}
func TestE2ESuite(t *testing.T) {
suite.Run(t, new(E2ETestSuite))
}
Test Helpers
Common Test Utilities
package helper
import (
"context"
"testing"
"time"
)
func WaitForCondition(t *testing.T, condition func() bool, timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
t.Fatal("timeout waiting for condition")
case <-ticker.C:
if condition() {
return
}
}
}
}
func RandomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
Best Practices
Isolation
Keep tests isolated and independent of each other
Coverage
Aim for high test coverage, especially for business logic
Fast Tests
Keep unit tests fast; use integration tests sparingly
Clear Names
Use descriptive test names that explain what is being tested
Next Steps
Deployment
Deploy your tested service
CI/CD
Set up continuous integration