| 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
type Storage interface {
Create(ctx context.Context, task Task) error
GetByID(ctx context.Context, id string) (*Task, error)
}
type PostgresStorage struct{ db *sql.DB }
func (s *PostgresStorage) GetByID(ctx context.Context, id string) (*Task, error) {
}
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
export interface TaskAPI {
createTask(title: string): Promise<Task>;
getTasks(): Promise<Task[]>;
}
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');
}
}
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
from typing import Protocol
class TaskStorage(Protocol):
def get_by_id(self, task_id: str) -> Task: ...
def create(self, task: Task) -> None: ...
class PostgresTaskStorage:
def __init__(self, db: AsyncEngine) -> None:
self._db = db
async def get_by_id(self, task_id: str) -> Task:
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
#[async_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>;
}
pub struct PostgresTaskStorage { pool: PgPool }
#[async_trait]
impl TaskStorage for PostgresTaskStorage {
async fn get_by_id(&self, id: &str) -> Result<Task, StorageError> {
}
}
pub struct InMemoryTaskStorage {
tasks: Mutex<HashMap<String, Task>>,
}
#[async_trait]
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)
}
}
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):
func calculateDiscount(ctx context.Context, items []Item, couponID string) (float64, error) {
validCoupon, err := db.GetCoupon(ctx, couponID)
...
}
Rule 3: Dependency Direction — Wiring Pattern
Dependencies are wired at the outermost layer (main.go, app.py, main.rs), not inside business logic:
storage := postgres.NewTaskStorage(db)
service := task.NewService(storage)
handler := task.NewHandler(service)
router.POST("/tasks", handler.Create)
storage = PostgresTaskStorage(engine)
service = TaskService(storage)
router.include_router(task_router(service))
const api = new BackendTaskAPI(httpClient);
const store = useTaskStore(api);
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