Add A-Frame architecture and Testing Without Mocks guidelines

Document James Shore's patterns for clean architecture:
- A-Frame: Logic and Infrastructure as independent peers
- Testing Without Mocks: state-based tests, Nullables, no mocking
- Testing approach by layer (Logic, Infrastructure, Application)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kevin Sivic 2025-11-30 22:51:32 -05:00
parent 67657bf717
commit 9cbbb045b9

View file

@ -34,6 +34,56 @@ A pre-commit hook is provided that runs `mix format --check-formatted` and `mix
- **Prefer small, focused commits** - each commit should represent a single logical change
- **Write tests first** - follow test-driven development (TDD) where practical; write failing tests before implementing features
- Use `mix precommit` alias when you are done with all changes and fix any pending issues
### Architecture: A-Frame Pattern
This project follows [James Shore's A-Frame Architecture](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks) where Logic and Infrastructure are independent peers coordinated at the Application layer:
```
Application/UI Values
/ \
V V
Logic Infrastructure
```
- **Logic** - Pure business logic with no external dependencies; easily testable with state-based tests
- **Infrastructure** - Wrappers around external systems (database, APIs, file system); one wrapper per external system
- **Application** - Coordinates Logic and Infrastructure; thin layer that wires things together
- **Values** - Immutable data structures passed between layers
### Testing: Without Mocks
Follow the "Testing Without Mocks" pattern language. Key principles:
- **Avoid mocks** - They test implementation details and break when refactoring
- **State-based tests** - Check output/state, not function calls
- **Narrow tests** - Focus on specific functions/behaviors, not system-wide functionality
- **Overlapping sociable tests** - Execute real dependencies; dependencies have their own thorough tests
#### Nullables Pattern
For infrastructure code, implement **Nullables** - production code with an off-switch for external communication:
- Create a factory method that returns a "null" version for testing (disables external calls but preserves behavior)
- Nullables have legitimate production uses (dry-run modes, cache warming)
- Test Nullables like any production code
#### Infrastructure Wrappers
- One wrapper module per external system (Repo for database, HTTP clients for APIs)
- Expose clean interfaces matching application needs, not third-party APIs
- These are the **only** places where external communication occurs
- Test with narrow integration tests against real systems (test database, etc.)
#### Testing Approach by Layer
- **Logic** - Pure state-based tests; no infrastructure needed
- **Infrastructure wrappers** - Narrow integration tests with real external systems
- **Application code** - Sociable tests with Nullable dependencies
- **LiveViews/Controllers** - Use Phoenix test helpers with test database
### Dependencies
- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps
### Phoenix v1.8 guidelines