agentleFS
Sign inSign up

go-ai-ready-boilerplate / rules

oniharnantyo/go-ai-ready-boilerplate/.cursor/rules/08-testing-standards.mdc

Mandatory testing framework: testify/suite + mockery-generated mocks

Cursor rule1 starsChanged 50 days ago
  • Installs packages
---
description: "Mandatory testing framework: testify/suite + mockery-generated mocks"
globs:
alwaysApply: true
---
# Rule: Testing Standards

## Mandatory Testing Framework

**ALL tests MUST use:**
- **testify** for assertions (`require` in test bodies, `assert` only where meaningfully better)
- **testify/suite** for test organization and structure
- **mockery** for interface mocking (use mockery tool to generate mocks from interfaces)

**NEVER use:**
- Other testing frameworks (except where explicitly required)
- Hand-written mocks in new test files (use mockery-generated mocks)
- Other assertion libraries

**Mock Generation Requirements:**
- Use `mockery` tool to generate mocks from interfaces
- Generate mocks with: `mockery --all --output=./mocks`
- Mock files are generated in `./mocks/` directory
- Import generated mocks as `package mocks_<package>`
- Regenerate mocks when interfaces change

## Test Structure Standards

Every test file MUST follow this structure:

```go
package <package>

import (
    "testing"
    "github.com/stretchr/testify/suite"
    // Other imports...
)

// <Feature>TestSuite tests the <feature> functionality.
type <Feature>TestSuite struct {
    suite.Suite
    // Add suite fields here
    mockDependency *MockDependency
    handler       *Handler
    echo          *echo.Echo
}

func (s *<Feature>TestSuite) SetupTest() {
    // Setup: Initialize mocks and test fixtures
}

func (s *<Feature>TestSuite) TearDownTest() {
    // Teardown: Clean up resources
}

// Test cases follow naming: Test_<MethodName>_<Scenario>
func (s *<Feature>TestSuite) Test_MethodName_Scenario() {
    // Arrange: Set up expectations
    // Act: Call the method
    // Assert: Verify results
}

func Test<Feature>TestSuite(t *testing.T) {
    suite.Run(t, new(<Feature>TestSuite))
}
```

## Mock Standards

**Mockery-Generated Mock Usage:**

1. **Generate mocks with mockery** from interfaces
2. **Import generated mocks** from `mocks_<package>` package
3. **Use testify/mock methods** on generated mocks
4. **Set expectations before calling** methods
5. **Assert expectations after** test execution

```go
// Import generated mocks (generated by mockery tool)
import "github.com/project/internal/application/ports/mocks_application_ports_driven_repositories"

// Test Usage
func (s *FeatureTestSuite) Test_CreateTask_Success() {
    // Create generated mock
    repo := new(mocks_application_ports_driven_repositories.MockTaskRepository)
    
    // Set up expectations
    repo.On("Create", mock.Anything, mock.MatchedBy(func(task domain.Task) bool {
        return task.Title == "Test Task"
    })).Return(nil)
    
    handler := NewHandler(repo)
    
    task, err := handler.CreateTask(context.Background(), "test")
    
    // Assert results
    s.NoError(err)
    s.Equal("Test Task", task.Title)
    
    // Verify mock was called as expected
    repo.AssertExpectations(s.T())
}
```

**Mockery Setup:**
```bash
# Install mockery
go install github.com/vektra/mockery/v2@latest

# Generate mocks from interfaces
mockery --all --output=./mocks --dir=./internal/application/ports/driven

# Or generate for specific package
mockery --name=TaskRepository --output=./mocks
```

**Generated Mock File Structure:**
```
internal/
  application/
    ports/
      driven/
        repositories/
          task_repository.go    # Interface definition
        mocks_application_ports_driven_repositories/
          MockTaskRepository.go  # Generated by mockery
```

## Assertion Standards

**Use `require`** in test bodies for critical assertions:
- Use `require.NoError()` for error checks
- Use `require.Equal()` for critical value comparisons
- Use `require.True()`/`require.False()` for boolean checks
- Only use `assert` when test should continue after failure

## Test Naming Conventions

- **Suite**: `<Feature>TestSuite`
- **Test methods**: `Test_<MethodBeingTested>_<Scenario>`
- **File**: `<feature>_test.go` (next to the code being tested)

## Setup/Teardown Patterns

```go
func (s *FeatureTestSuite) SetupTest() {
    // Initialize mocks
    s.mockDependency = &MockDependency{}
    
    // Create handler/subject under test
    s.handler = NewHandler(s.mockDependency)
    
    // Set up Echo for HTTP tests
    s.echo = echo.New()
}

func (s *FeatureTestSuite) TearDownTest() {
    // Clean up
    s.echo = nil
    s.handler = nil
}
```

## Test Organization

**Group related tests in suite methods:**
```go
func (s *FeatureTestSuite) Test_Create_Scenario() { }
func (s *FeatureTestSuite) Test_Create_AnotherScenario() { }
func (s *FeatureTestSuite) Test_Update_Scenario() { }
func (s *FeatureTestSuite) Test_Delete_Scenario() { }
```

## HTTP Handler Testing Patterns

**Standard HTTP handler test pattern:**
```go
func (s *HandlerTestSuite) Test_Create_Returns201() {
    // Arrange
    expected := domain.Task{ID: "1", Title: "Test"}
    s.mockService.On("Create", mock.Anything, mock.Anything).Return(expected, nil)
    
    // Act
    req := httptest.NewRequest(http.MethodPost, "/resource", body)
    rec := httptest.NewRecorder()
    c := s.echo.NewContext(req, rec)
    
    err := s.handler.Create(c)
    
    // Assert
    s.NoError(err)
    s.Equal(http.StatusCreated, rec.Code)
    s.mockService.AssertExpectations(s.T())
}
```

## UseCase Testing Patterns

**Standard use case test pattern with mockery:**
```go
import "github.com/project/internal/application/ports/mocks_application_ports_driven_repositories"

func (s *UseCaseTestSuite) Test_Execute_Success() {
    // Arrange
    repo := new(mocks_application_ports_driven_repositories.MockTaskRepository)
    publisher := new(mocks_application_ports_driven_repositories.MockTaskEventPublisher)
    usecase := NewCreateTaskUseCase(repo, publisher)
    
    input := CreateTaskInput{Title: "Test"}
    
    // Set up expectations
    repo.On("Create", mock.Anything, mock.MatchedBy(func(task domain.Task) bool {
        return task.Title == "Test"
    })).Return(nil)
    publisher.On("PublishTaskCreated", mock.Anything, mock.Anything).Return(nil)
    
    // Act
    result, err := usecase.Execute(context.Background(), input)
    
    // Assert
    s.NoError(err)
    s.Equal("Test", result.Title)
    
    // Verify expectations
    repo.AssertExpectations(s.T())
    publisher.AssertExpectations(s.T())
}
```

## Error Testing Standards

**Always test error paths:**
```go
func (s *FeatureTestSuite) Test_Method_ReturnsError() {
    s.mockDependency.On("Method", mock.Anything).Return(errors.New("test error"))
    
    err := s.handler.DoSomething()
    
    s.Error(err)
    s.Contains(err.Error(), "test error")
}
```

## Context and Cancellation Testing

**Test context behavior:**
```go
func (s *FeatureTestSuite) Test_Method_HandlesContextCancellation() {
    ctx, cancel := context.WithCancel(context.Background())
    cancel() // Cancel immediately
    
    err := s.handler.Method(ctx)
    
    s.Error(err)
}
```

## Integration vs Unit Testing

**Unit tests** (most common):
- Mock all external dependencies
- Fast execution
- Test single behavior

**Integration tests** (rare):
- Use real implementations
- Test across boundaries
- Keep in separate files: `<feature>_integration_test.go`

## Test Coverage Requirements

- **New code**: MUST have tests before considered complete
- **Bug fixes**: MUST add regression test
- **Features**: Table-driven tests for normal/edge/error cases
- **HTTP handlers**: Test all status codes and error paths
- **Use cases**: Test success path, domain errors, and infrastructure errors

## Forbidden Testing Patterns

❌ **NEVER do this:**
```go
// Hand-written mocks (for new code)
type fakeDependency struct{}
func (f *fakeDependency) Method() error { return nil }

// Other frameworks
testing.T().Errorf("expected %s", got) // Use testify assertions

// Complex mock logic in tests
if condition {
    mock.Return("value1")
} else {
    mock.Return("value2")
}
```

✅ **ALWAYS do this:**
```go
// Use mockery-generated mocks
import "github.com/project/internal/ports/mocks_ports"
mockDep := new(mocks_ports.MockDependency)
mockDep.On("Method").Return("value")

// testify assertions
s.NoError(err)
s.Equal("expected", got)
s.True(condition)

// Use mock.MatchedBy for complex matching
mockDep.On("Method", mock.MatchedBy(func(arg string) bool {
    return arg == "expected_value"
})).Return(nil)
```

## Test Performance Requirements

- Tests MUST run quickly (aim for <100ms per suite)
- Use `t.Parallel()` for independent test cases
- Avoid sleep/polling in tests
- Mock external dependencies

## Documentation in Tests

- **Test suite**: Document what functionality is being tested
- **Complex test cases**: Add comments explaining the scenario
- **Edge cases**: Document why the test is necessary

## Quality Checklist

Before considering a test complete:
- [ ] Uses testify/suite
- [ ] Uses testify/mock for interfaces
- [ ] Uses require for critical assertions
- [ ] Has proper SetupTest/TearDownTest
- [ ] Tests both success and failure paths
- [ ] Mocks all external dependencies
- [ ] Asserts on mock expectations
- [ ] Follows naming conventions
- [ ] Runs quickly (<100ms per suite)

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.