Skip to main content

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.

Datos de origen

Repositorio
irahardianto/awesome-agv
Última actividad en el origen
30 de julio de 2026 a las 14:18
Idioma detectado de SKILL.md
inglés
Estrellas
157
Forks
53

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
testability-patterns
description
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.
## 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 ```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 ```typescript // 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 ```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 ```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) ```dart // 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):** ```go // ❌ 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: ```go // 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) ``` ```python # main.py — Wiring storage = PostgresTaskStorage(engine) # Infrastructure service = TaskService(storage) # Business router.include_router(task_router(service)) # Delivery ``` ```typescript // 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
Ver en GitHub