LLMSkillHub
mirroredCopied from irahardianto/awesome-agv under MIT. irahardianto has not claimed this page, and is not affiliated with LLM SkillHub.

irahardianto / testability-patterns

Implementation examples for the three testability rules (I/O isolation, pure logic, dependency direction) across Go, TypeScript, Python, Rust, and Dart. Loaded by reference from architectural-pattern.md — not triggered by file patterns.

0
SKILL.md
7.0 KB

Testability Patterns — Implementation Examples

Language-specific examples for the three testability rules defined in @.agents/rules/architectural-pattern.md. Load this skill when you need concrete implementation reference for I/O isolation, pure business logic, or dependency direction in a specific language.


Rule 1: I/O Isolation — Interface + Adapter Pattern

Go
// storage.go — Contract defined in the consumer feature
type Storage interface {
    Create(ctx context.Context, task Task) error
    GetByID(ctx context.Context, id string) (*Task, error)
}

// storage_pg.go — Production adapter
type PostgresStorage struct{ db *sql.DB }

func (s *PostgresStorage) GetByID(ctx context.Context, id string) (*Task, error) {
    // real database query
}

// storage_mock.go — Test adapter
type MockStorage struct {
    tasks map[string]*Task
}

func (m *MockStorage) GetByID(ctx context.Context, id string) (*Task, error) {
    t, ok := m.tasks[id]
    if !ok {
        return nil, ErrNotFound
    }
    return t, nil
}
TypeScript / Vue
// task.api.ts — Contract (service layer)
export interface TaskAPI {
    createTask(title: string): Promise<Task>;
    getTasks(): Promise<Task[]>;
}

// task.api.backend.ts — Production adapter
export class BackendTaskAPI implements TaskAPI {
    async createTask(title: string): Promise<Task> {
        return this.http.post('/api/tasks', { title });
    }
    async getTasks(): Promise<Task[]> {
        return this.http.get('/api/tasks');
    }
}

// In tests — Test adapter (vi.mock or manual)
export class MockTaskAPI implements TaskAPI {
    private tasks: Task[] = [];
    async createTask(title: string): Promise<Task> {
        const task = { id: crypto.randomUUID(), title };
        this.tasks.push(task);
        return task;
    }
    async getTasks(): Promise<Task[]> {
        return [...this.tasks];
    }
}
Python
# storage.py — Contract via Protocol
from typing import Protocol

class TaskStorage(Protocol):
    def get_by_id(self, task_id: str) -> Task: ...
    def create(self, task: Task) -> None: ...

# storage_pg.py — Production adapter
class PostgresTaskStorage:
    def __init__(self, db: AsyncEngine) -> None:
        self._db = db

    async def get_by_id(self, task_id: str) -> Task:
        # real database query

# In tests — InMemory adapter
class InMemoryTaskStorage:
    def __init__(self) -> None:
        self._tasks: dict[str, Task] = {}

    async def get_by_id(self, task_id: str) -> Task:
        if task_id not in self._tasks:
            raise TaskNotFoundError(task_id)
        return self._tasks[task_id]
Rust
// storage.rs — Contract via trait
pub trait TaskStorage: Send + Sync {
    async fn get_by_id(&self, id: &str) -> Result<Task, StorageError>;
    async fn create(&self, task: &Task) -> Result<(), StorageError>;
}

// storage_pg.rs — Production adapter (sqlx)
// The full PostgresTaskStorage implementation (sqlx::query_as!, pool wiring,
// error mapping, transaction handling) lives in a single source of truth:
// @.agents/skills/rust-idioms/references/sqlx-patterns.md §Repository Pattern.
// Do not duplicate the production adapter here — this skill shows only the
// contract + test double that make the trait testable in isolation.

// In tests — In-memory adapter
pub struct InMemoryTaskStorage {
    tasks: Mutex<HashMap<String, Task>>,
}

impl TaskStorage for InMemoryTaskStorage {
    async fn get_by_id(&self, id: &str) -> Result<Task, StorageError> {
        self.tasks.lock().unwrap()
            .get(id)
            .cloned()
            .ok_or(StorageError::NotFound)
    }
}

async fn in traits (Rust 1.75+): Native async fn in traits uses static dispatch — no async-trait crate required for impl Trait / T: Trait. Use #[async_trait] only when you need dynamic dispatch via dyn Trait (Box<dyn TaskStorage>, Arc<dyn TaskStorage>). See @.agents/skills/rust-idioms/SKILL.md §Toolchain (MSRV milestones) for the single source of truth on this policy.

Flutter / Dart (Riverpod)
// task_repository.dart — Contract
abstract interface class TaskRepository {
  Future<Task> getById(String id);
  Future<void> create(Task task);
}

// task_repository_api.dart — Production adapter
class ApiTaskRepository implements TaskRepository {
  final Dio _client;
  @override
  Future<Task> getById(String id) async {
    final response = await _client.get('/tasks/$id');
    return Task.fromJson(response.data);
  }
}

// In tests — Fake adapter
class FakeTaskRepository implements TaskRepository {
  final _tasks = <String, Task>{};
  @override
  Future<Task> getById(String id) async {
    final task = _tasks[id];
    if (task == null) throw NotFoundException(id);
    return task;
  }
}

Rule 2: Pure Business Logic — Fetch → Calculate → Persist

The pattern is universal. Always separate the three concerns:

// 1. Fetch dependencies (in handler/service — has I/O access)
validCoupon, err := store.GetCoupon(ctx, coupon.ID)
user, err := store.GetUser(ctx, userID)

// 2. Pass to pure logic (in logic file — no I/O)
discount, err := calculateDiscount(items, validCoupon, user.Tier)
// ✅ calculateDiscount is pure: same inputs → same output, no side effects

// 3. Persist result (in handler/service — has I/O access)
err = store.SaveOrder(ctx, order)

Anti-pattern — I/O buried in logic (untestable):

// ❌ Cannot test calculateDiscount without a real database
func calculateDiscount(ctx context.Context, items []Item, couponID string) (float64, error) {
    validCoupon, err := db.GetCoupon(ctx, couponID) // NO — db call inside logic
    ...
}

Rule 3: Dependency Direction — Wiring Pattern

Dependencies are wired at the outermost layer (main.go, app.py, main.rs), not inside business logic:

// cmd/api/main.go — Wiring
storage := postgres.NewTaskStorage(db)      // Infrastructure
service := task.NewService(storage)          // Business (depends on contract, not postgres)
handler := task.NewHandler(service)          // Delivery
router.POST("/tasks", handler.Create)
# main.py — Wiring
storage = PostgresTaskStorage(engine)        # Infrastructure
service = TaskService(storage)               # Business
router.include_router(task_router(service))  # Delivery
// main.ts — Wiring (or DI container)
const api = new BackendTaskAPI(httpClient);      // Infrastructure
const store = useTaskStore(api);                  // Business (Pinia)

Related

  • Architectural Patterns @.agents/rules/architectural-pattern.md (the three rules)
  • Testing Strategy @.agents/rules/testing-strategy.md
  • Code Organization Principles @.agents/rules/code-organization-principles.md