testability-patterns
SkillFiles & storageImplementation 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the testability-patterns skill
What this skill tells your AI
The instructions your AI receives, as published by irahardianto/awesome-agv in .agents/skills/testability-patterns/SKILL.md and read by ahel’s review.
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 fnin traits (Rust 1.75+): Nativeasync fnin traits uses static dispatch — noasync-traitcrate required forimpl Trait/T: Trait. Use#[async_trait]only when you need dynamic dispatch viadyn 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
Signals
- GitHub stars
- 156
- Forks
- 53
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
testability-patterns- Source
- github.com/irahardianto/awesome-agv