xiaohongshu
xiaohongshu
kratos-tests
Generates comprehensive table-driven tests for go-kratos microservices using testify/mock and testify/assert. Creates repository tests with in-memory SQLite, business layer tests with mocks, and service layer tests with proper context propagation. Use when writing unit tests for any kratos layer,...
Full skill instructions
Generate comprehensive, table-driven tests for all layers of a Kratos microservice (repository, business logic, service) using testify/assert and testify/mock libraries.
File: `internal/data/repo/{entity}_test.go`
package repo
import (
"context"
"testing"
"{service}/internal/biz/domain"
"{service}/internal/data/model"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Setup in-memory database
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Errorf("Failed to open in-memory SQLite database: %v", err)
}
if err := db.AutoMigrate(&model.{Entity}{}, &model.{RelatedEntity}{}); err != nil {
t.Errorf("Failed to migrate test tables: %v", err)
}
return db
}
// Cleanup function
func cleanupDB(db *gorm.DB) {
db.Exec("DELETE FROM {related_entities}")
db.Exec("DELETE FROM {entities}")
}
// Helper: valid domain object
func validDomain{Entity}() *domain.{Entity} {
return &domain.{Entity}{
Field1: "value1",
Field2: 123,
// ... all required fields
}
}
// Helper: valid GORM entity
func validEntity{Entity}() *model.{Entity} {
return &model.{Entity}{
Field1: "value1",
Field2: 123,
// ... all required fields
}
}
func TestCreate(t *testing.T) {
tests := []struct {
name string
input *domain.{Entity}
setup func(*gorm.DB)
wantErr bool
checkError func(*testing.T, error)
checkResult func(*testing.T, *domain.{Entity})
}{
{
name: "success with nested data",
input: validDomain{Entity}(),
setup: func(db *gorm.DB) {},
wantErr: false,
checkResult: func(t *testing.T, result *domain.{Entity}) {
assert.NotNil(t, result)
assert.NotZero(t, result.ID)
assert.Equal(t, "value1", result.Field1)
},
},
{
name: "duplicate entry error",
input: validDomain{Entity}(),
setup: func(db *gorm.DB) {
// Pre-create entity to cause duplicate
entity := validEntity{Entity}()
db.Create(entity)
},
wantErr: true,
checkError: func(t *testing.T, err error) {
assert.ErrorIs(t, err, domain.ErrDataDuplicateEntry)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := setupTestDB(t)
defer cleanupDB(db)
tt.setup(db)
logger := log.NewStdLogger(os.Stdout)
tx := &mockTransaction{}
repo := New{Entity}Repo(db, tx, logger)
result, err := repo.Create(context.Background(), tt.input)
if tt.wantErr {
assert.Error(t, err)
if tt.checkError != nil {
tt.checkError(t, err)
}
} else {
assert.NoError(t, err)
if tt.checkResult != nil {
tt.checkResult(t, result)
}
}
})
}
}
func TestList{Entities}(t *testing.T) {
tests := []struct {
name string
offset uint64
limit uint32
filter map[string]interface{}
setup func(*gorm.DB)
wantErr bool
checkResult func(*testing.T, []*domain.{Entity}, *pagination.Meta)
}{
{
name: "success - first page with next page",
offset: 0,
limit: 5,
filter: map[string]interface{}{"project_id": uint64(1)},
setup: func(db *gorm.DB) {
for i := 0; i < 10; i++ {
entity := validEntity{Entity}()
entity.UID = fmt.Sprintf("uid-%d", i)
db.Create(entity)
}
},
wantErr: false,
checkResult: func(t *testing.T, results []*biz.{Entity}, meta *pagination.Meta) {
assert.Len(t, results, 5)
assert.Equal(t, uint64(10), meta.TotalCount)
assert.True(t, meta.HasNextPage)
assert.False(t, meta.HasPreviousPage)
},
},
{
name: "success - filter with pagination",
offset: 0,
limit: 10,
filter: map[string]interface{}{"project_id": uint64(1), "label": "test"},
setup: func(db *gorm.DB) {
// Create entities with matching filter
for i := 0; i < 3; i++ {
entity := validEntity{Entity}()
entity.Label = "test"
db.Create(entity)
}
// Create entities not matching filter
for i := 0; i < 5; i++ {
entity := validEntity{Entity}()
entity.Label = "other"
db.Create(entity)
}
},
wantErr: false,
checkResult: func(t *testing.T, results []*biz.{Entity}, meta *pagination.Meta) {
assert.Len(t, results, 3)
assert.Equal(t, uint64(3), meta.TotalCount) // Only filtered count
for _, r := range results {
assert.Equal(t, "test", r.Label)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := setupTestDB(t)
defer cleanupDB(db)
tt.setup(db)
logger := log.NewStdLogger(os.Stdout)
tx := &mockTransaction{}
repo := New{Entity}Repo(db, tx, logger)
results, meta, err := repo.List{Entities}(context.Background(), tt.offset, tt.limit, tt.filter)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.checkResult != nil {
tt.checkResult(t, results, meta)
}
}
})
}
}
File: `internal/biz/{entity}/usecase_test.go` (new structure) Old File: `internal/biz/{entity}_test.go` (legacy structure)
// Package {entity} provides tests for use cases managing {entities}.
package {entity}
import (
"context"
"testing"
"platform/pagination"
"{service}/internal/biz/domain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock repository
type Mock{Entity}Repo struct {
mock.Mock
}
func (m *Mock{Entity}Repo) Create(ctx context.Context, entity *domain.{Entity}) (*domain.{Entity}, error) {
args := m.Called(ctx, entity)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.{Entity}), args.Error(1)
}
func (m *Mock{Entity}Repo) FindByID(ctx context.Context, id uint64) (*domain.{Entity}, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.{Entity}), args.Error(1)
}
func (m *Mock{Entity}Repo) List{Entities}(ctx context.Context, offset uint64, limit uint32, filter map[string]interface{}) ([]*domain.{Entity}, *pagination.Meta, error) {
args := m.Called(ctx, offset, limit, filter)
if args.Get(0) == nil {
return nil, nil, args.Error(2)
}
if args.Get(1) == nil {
return args.Get(0).([]*domain.{Entity}), nil, args.Error(2)
}
return args.Get(0).([]*domain.{Entity}), args.Get(1).(*pagination.Meta), args.Error(2)
}
// Test helper - legacy (without event publishing)
func setup{Entity}UseCase(mockRepo *Mock{Entity}Repo) domain.{Entity}UseCase {
logger := log.NewStdLogger(os.Stdout)
validator := NewValidator()
mockTx := new(MockTransaction)
mockTx.On("InTx", mock.Anything, mock.Anything).Return(nil).Maybe()
return NewUseCase(mockRepo, validator, mockTx, logger)
}
func valid{Entity}() *domain.{Entity} {
return &domain.{Entity}{
Field1: "value1",
Field2: 123,
}
}
func TestCreate{Entity}(t *testing.T) {
tests := []struct {
name string
input *domain.{Entity}
mockSetup func(*Mock{Entity}Repo, context.Context, *domain.{Entity})
wantErr bool
errContains string
checkResult func(*testing.T, *domain.{Entity})
}{
{
name: "success",
input: valid{Entity}(),
mockSetup: func(repo *Mock{Entity}Repo, ctx context.Context, input *{Entity}) {
output := valid{Entity}()
output.Id = 1
repo.On("Create", ctx, input).Return(output, nil)
},
wantErr: false,
checkResult: func(t *testing.T, result *domain.{Entity}) {
assert.NotZero(t, result.ID)
assert.Equal(t, "value1", result.Field1)
},
},
{
name: "validation error - missing required field",
input: &{Entity}{
Field2: 123,
// Field1 missing
},
mockSetup: func(repo *Mock{Entity}Repo, ctx context.Context, input *{Entity}) {},
wantErr: true,
errContains: "Field1",
},
{
name: "repository error",
input: valid{Entity}(),
mockSetup: func(repo *Mock{Entity}Repo, ctx context.Context, input *{Entity}) {
repo.On("Create", ctx, input).Return(nil, errors.New("database error"))
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := new(Mock{Entity}Repo)
uc := setup{Entity}UseCase(mockRepo)
ctx := context.Background()
tt.mockSetup(mockRepo, ctx, tt.input)
result, err := uc.Create{Entity}(ctx, tt.input)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
assert.NoError(t, err)
if tt.checkResult != nil {
tt.checkResult(t, result)
}
}
mockRepo.AssertExpectations(t)
})
}
}
CRITICAL: Test that use case correctly passes filters to repository
func TestList{Entities}(t *testing.T) {
tests := []struct {
name string
params *pagination.OffsetPaginationParams
filter map[string]interface{}
mockSetup func(*Mock{Entity}Repo, context.Context, uint64, uint32, map[string]interface{})
wantErr bool
checkResult func(*testing.T, []*{Entity}, *pagination.Meta)
}{
{
name: "success - passes filter to repository",
params: &pagination.OffsetPaginationParams{
Offset: 0,
Limit: 10,
},
filter: map[string]interface{}{"project_id": uint64(1)},
mockSetup: func(repo *Mock{Entity}Repo, ctx context.Context, offset uint64, limit uint32, filter map[string]interface{}) {
entities := []*domain.{Entity}{valid{Entity}()}
meta := &pagination.Meta{TotalCount: 1, Offset: 0, Limit: 10}
// Verify exact filter is passed through
repo.On("List{Entities}", ctx, offset, limit, filter).Return(entities, meta, nil)
},
wantErr: false,
},
{
name: "success - passes nil filter to repository",
params: &pagination.OffsetPaginationParams{Offset: 0, Limit: 10},
filter: nil,
mockSetup: func(repo *Mock{Entity}Repo, ctx context.Context, offset uint64, limit uint32, filter map[string]interface{}) {
entities := []*{Entity}{valid{Entity}()}
meta := &pagination.Meta{TotalCount: 1}
repo.On("List{Entities}", ctx, offset, limit, filter).Return(entities, meta, nil)
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := new(Mock{Entity}Repo)
uc := setup{Entity}UseCase(mockRepo)
ctx := context.Background()
tt.mockSetup(mockRepo, ctx, tt.params.Offset, tt.params.Limit, tt.filter)
results, meta, err := uc.List{Entities}(ctx, tt.params, tt.filter)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.checkResult != nil {
tt.checkResult(t, results, meta)
}
}
mockRepo.AssertExpectations(t)
})
}
}
When: Use cases integrate event publishing for domain events
Mock Setup:
// Mock event publisher
type MockPublisher struct {
mock.Mock
}
func (m *MockPublisher) PublishSymbolCreated(ctx context.Context, symbol *domain.Symbol) error {
args := m.Called(ctx, symbol)
return args.Error(0)
}
func (m *MockPublisher) PublishSymbolUpdated(ctx context.Context, symbol *domain.Symbol) error {
args := m.Called(ctx, symbol)
return args.Error(0)
}
func (m *MockPublisher) PublishSymbolDeleted(ctx context.Context, symbol *domain.Symbol) error {
args := m.Called(ctx, symbol)
return args.Error(0)
}
// Mock transaction
type MockTransaction struct {
mock.Mock
}
func (m *MockTransaction) InTx(ctx context.Context, fn func(ctx context.Context) error) error {
args := m.Called(ctx, fn)
if args.Error(0) != nil {
return args.Error(0)
}
// Execute callback immediately without real transaction
return fn(ctx)
}
// Test setup with all dependencies
type testDeps struct {
repo *MockSymbolRepo
pub *MockPublisher
tx *MockTransaction
uc domain.SymbolUseCase
}
func setupSymbolUseCaseWithDeps() *testDeps {
logger := log.NewStdLogger(os.Stdout)
v := NewValidator()
mockRepo := new(MockSymbolRepo)
mockPub := new(MockPublisher)
mockTx := new(MockTransaction)
// Default transaction behavior - executes callback
mockTx.On("InTx", mock.Anything, mock.Anything).Return(nil).Maybe()
uc := NewUseCase(mockRepo, v, mockTx, mockPub, logger)
return &testDeps{
repo: mockRepo,
pub: mockPub,
tx: mockTx,
uc: uc,
}
}
Test Pattern - Event Publishing Success:
func TestCreateSymbol_PublishesEvent(t *testing.T) {
tests := []struct {
name string
symbol *domain.Symbol
repoReturn *domain.Symbol
publishErr error
wantErr bool
wantEventCall bool
checkEventData func(*testing.T, *domain.Symbol)
}{
{
name: "publishes SymbolCreated event on success",
symbol: validSymbol(),
repoReturn: func() *domain.Symbol {
s := validSymbol()
s.ID = 1
return s
}(),
publishErr: nil,
wantErr: false,
wantEventCall: true,
checkEventData: func(t *testing.T, published *domain.Symbol) {
assert.Equal(t, uint64(1), published.ID)
assert.Equal(t, "Test Symbol", published.Label)
},
},
{
name: "rolls back transaction on publish failure",
symbol: validSymbol(),
repoReturn: func() *domain.Symbol {
s := validSymbol()
s.ID = 1
return s
}(),
publishErr: errors.New("publish failed"),
wantErr: true,
wantEventCall: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps := setupSymbolUseCaseWithDeps()
ctx := context.Background()
// Setup repo mock
deps.repo.On("Create", ctx, mock.AnythingOfType("*domain.Symbol")).Return(tt.repoReturn, nil)
// Setup publisher mock - capture published symbol
var publishedSymbol *domain.Symbol
deps.pub.On("PublishSymbolCreated", ctx, mock.AnythingOfType("*domain.Symbol")).
Run(func(args mock.Arguments) {
publishedSymbol = args.Get(1).(*domain.Symbol)
}).
Return(tt.publishErr)
result, err := deps.uc.CreateSymbol(ctx, tt.symbol)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
if tt.wantEventCall {
deps.pub.AssertCalled(t, "PublishSymbolCreated", ctx, mock.AnythingOfType("*domain.Symbol"))
if tt.checkEventData != nil && publishedSymbol != nil {
tt.checkEventData(t, publishedSymbol)
}
}
deps.repo.AssertExpectations(t)
deps.pub.AssertExpectations(t)
})
}
}
Test Pattern - Event Not Published on Failure:
func TestEventPublishing_NotCalledOnValidationFailure(t *testing.T) {
t.Run("CreateSymbol does not publish event on validation failure", func(t *testing.T) {
deps := setupSymbolUseCaseWithDeps()
ctx := context.Background()
invalidSymbol := &domain.Symbol{
Label: "", // Invalid - missing required field
}
_, err := deps.uc.CreateSymbol(ctx, invalidSymbol)
assert.Error(t, err)
deps.pub.AssertNotCalled(t, "PublishSymbolCreated", mock.Anything, mock.Anything)
})
t.Run("CreateSymbol does not publish event on repo failure", func(t *testing.T) {
deps := setupSymbolUseCaseWithDeps()
ctx := context.Background()
symbol := validSymbol()
deps.repo.On("Create", ctx, mock.AnythingOfType("*domain.Symbol")).
Return(nil, errors.New("database error"))
_, err := deps.uc.CreateSymbol(ctx, symbol)
assert.Error(t, err)
deps.pub.AssertNotCalled(t, "PublishSymbolCreated", mock.Anything, mock.Anything)
})
}
Delete with Event Publishing:
func TestDeleteSymbol_PublishesEvent(t *testing.T) {
tests := []struct {
name string
symbolID uint64
foundSymbol *domain.Symbol
publishErr error
wantErr bool
checkEventData func(*testing.T, *domain.Symbol)
}{
{
name: "publishes SymbolDeleted event on success",
symbolID: 1,
foundSymbol: func() *domain.Symbol {
s := validSymbol()
s.ID = 1
return s
}(),
publishErr: nil,
wantErr: false,
checkEventData: func(t *testing.T, published *domain.Symbol) {
assert.Equal(t, uint64(1), published.ID)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps := setupSymbolUseCaseWithDeps()
ctx := context.Background()
// Must find symbol before delete (for event payload)
deps.repo.On("FindByID", ctx, tt.symbolID).Return(tt.foundSymbol, nil)
deps.repo.On("Delete", ctx, tt.symbolID).Return(nil)
// Capture published symbol
var publishedSymbol *domain.Symbol
deps.pub.On("PublishSymbolDeleted", ctx, mock.AnythingOfType("*domain.Symbol")).
Run(func(args mock.Arguments) {
publishedSymbol = args.Get(1).(*domain.Symbol)
}).
Return(tt.publishErr)
err := deps.uc.DeleteSymbol(ctx, tt.symbolID)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.checkEventData != nil && publishedSymbol != nil {
tt.checkEventData(t, publishedSymbol)
}
deps.repo.AssertExpectations(t)
deps.pub.AssertExpectations(t)
})
}
}
Key Testing Principles:
PublishXxxEvent is calledAssertNotCalledFile: `internal/service/{entity}_test.go`
type mock{Entity}UseCase struct {
mock.Mock
}
func (uc *mock{Entity}UseCase) List{Entities}(ctx context.Context, params *pagination.OffsetPaginationParams, filter map[string]interface{}) ([]*biz.{Entity}, *pagination.Meta, error) {
args := uc.Called(ctx, params, filter)
if args.Get(0) == nil {
return nil, nil, args.Error(2)
}
if args.Get(1) == nil {
return args.Get(0).([]*biz.{Entity}), nil, args.Error(2)
}
return args.Get(0).([]*biz.{Entity}), args.Get(1).(*pagination.Meta), args.Error(2)
}
// Interface in biz/interfaces.go
List{Entities}(ctx context.Context, offset uint64, limit uint32, filter map[string]interface{}) ([]*{Entity}, *pagination.Meta, error)
// Mock MUST have identical signature
func (m *Mock{Entity}Repo) List{Entities}(ctx context.Context, offset uint64, limit uint32, filter map[string]interface{}) ([]*{Entity}, *pagination.Meta, error) {
args := m.Called(ctx, offset, limit, filter) // Pass ALL parameters
// ...
}
mockRepo.AssertExpectations(t) // Verify all expected calls happened
❌ DON'T:
✅ DO:
Tests MUST:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience