feat(rules): add initial rule files for concerns, languages, and frameworks
Concerns (6 files): - coding-style.md (163 lines): patterns, anti-patterns, error handling, SOLID - naming.md (105 lines): naming conventions table per language - documentation.md (149 lines): docstrings, WHY vs WHAT, README standards - testing.md (134 lines): AAA pattern, mocking philosophy, TDD - git-workflow.md (118 lines): conventional commits, branch naming, PR format - project-structure.md (82 lines): directory layout, entry points, config placement Languages (4 files): - python.md (224 lines): uv, ruff, pyright, pytest, pydantic, idioms, anti-patterns - typescript.md (150 lines): strict mode, discriminated unions, satisfies, as const - nix.md (129 lines): flake structure, module patterns, alejandra, anti-patterns - shell.md (100 lines): set -euo pipefail, shellcheck, quoting, POSIX Frameworks (1 file): - n8n.md (42 lines): workflow design, node patterns, Error Trigger, security Context budget: 975 lines (concerns + python) < 1500 limit Refs: T6-T16 of rules-system plan
This commit is contained in:
134
rules/concerns/testing.md
Normal file
134
rules/concerns/testing.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Testing Rules
|
||||
|
||||
## Arrange-Act-Assert Pattern
|
||||
|
||||
Structure every test in three distinct phases:
|
||||
|
||||
```python
|
||||
# Arrange: Set up the test data and conditions
|
||||
user = User(name="Alice", role="admin")
|
||||
session = create_test_session(user.id)
|
||||
|
||||
# Act: Execute the behavior under test
|
||||
result = grant_permission(session, "read_documents")
|
||||
|
||||
# Assert: Verify the expected outcome
|
||||
assert result.granted is True
|
||||
assert result.permissions == ["read_documents"]
|
||||
```
|
||||
|
||||
Never mix phases. Comment each phase clearly for complex setups. Keep Act phase to one line if possible.
|
||||
|
||||
## Behavior vs Implementation Testing
|
||||
|
||||
Test behavior, not implementation details:
|
||||
|
||||
```python
|
||||
# GOOD: Tests the observable behavior
|
||||
def test_user_can_login():
|
||||
response = login("alice@example.com", "password123")
|
||||
assert response.status_code == 200
|
||||
assert "session_token" in response.cookies
|
||||
|
||||
# BAD: Tests internal implementation
|
||||
def test_login_sets_database_flag():
|
||||
login("alice@example.com", "password123")
|
||||
user = User.get(email="alice@example.com")
|
||||
assert user._logged_in_flag is True # Private field
|
||||
```
|
||||
|
||||
Focus on inputs and outputs. Test public contracts. Refactor internals freely without breaking tests.
|
||||
|
||||
## Mocking Philosophy
|
||||
|
||||
Mock external dependencies, not internal code:
|
||||
|
||||
```python
|
||||
# GOOD: Mock external services
|
||||
@patch("requests.post")
|
||||
def test_sends_notification_to_slack(mock_post):
|
||||
send_notification("Build complete!")
|
||||
mock_post.assert_called_once_with(
|
||||
"https://slack.com/api/chat.postMessage",
|
||||
json={"text": "Build complete!"}
|
||||
)
|
||||
|
||||
# BAD: Mock internal methods
|
||||
@patch("NotificationService._format_message")
|
||||
def test_notification_formatting(mock_format):
|
||||
# Don't mock private methods
|
||||
send_notification("Build complete!")
|
||||
```
|
||||
|
||||
Mock when:
|
||||
- Dependency is slow (database, network, file system)
|
||||
- Dependency is unreliable (external APIs)
|
||||
- Dependency is expensive (third-party services)
|
||||
|
||||
Don't mock when:
|
||||
- Testing the dependency itself
|
||||
- The dependency is fast and stable
|
||||
- The mock becomes more complex than real implementation
|
||||
|
||||
## Coverage Expectations
|
||||
|
||||
Write tests for:
|
||||
- Critical business logic (aim for 90%+)
|
||||
- Edge cases and error paths (aim for 80%+)
|
||||
- Public APIs and contracts (aim for 100%)
|
||||
|
||||
Don't obsess over:
|
||||
- Trivial getters/setters
|
||||
- Generated code
|
||||
- One-line wrappers
|
||||
|
||||
Coverage is a floor, not a ceiling. A test suite at 100% coverage that doesn't verify behavior is worthless.
|
||||
|
||||
## Test-Driven Development
|
||||
|
||||
Follow the red-green-refactor cycle:
|
||||
1. Red: Write failing test for new behavior
|
||||
2. Green: Write minimum code to pass
|
||||
3. Refactor: improve code while tests stay green
|
||||
|
||||
Write tests first for new features. Write tests after for bug fixes. Never refactor without tests.
|
||||
|
||||
## Test Organization
|
||||
|
||||
Group tests by feature or behavior, not by file structure. Name tests to describe the scenario:
|
||||
|
||||
```python
|
||||
class TestUserAuthentication:
|
||||
def test_valid_credentials_succeeds(self):
|
||||
pass
|
||||
|
||||
def test_invalid_credentials_fails(self):
|
||||
pass
|
||||
|
||||
def test_locked_account_fails(self):
|
||||
pass
|
||||
```
|
||||
|
||||
Each test should stand alone. Avoid shared state between tests. Use fixtures or setup methods to reduce duplication.
|
||||
|
||||
## Test Data
|
||||
|
||||
Use realistic test data that reflects production scenarios:
|
||||
|
||||
```python
|
||||
# GOOD: Realistic values
|
||||
user = User(
|
||||
email="alice@example.com",
|
||||
name="Alice Smith",
|
||||
age=28
|
||||
)
|
||||
|
||||
# BAD: Placeholder values
|
||||
user = User(
|
||||
email="test@test.com",
|
||||
name="Test User",
|
||||
age=999
|
||||
)
|
||||
```
|
||||
|
||||
Avoid magic strings and numbers. Use named constants for expected values that change often.
|
||||
Reference in New Issue
Block a user