Files
AGENTS/rules/concerns/testing.md
m3tm3re 8910413315 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
2026-02-17 19:05:45 +01:00

3.5 KiB

Testing Rules

Arrange-Act-Assert Pattern

Structure every test in three distinct phases:

# 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:

# 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:

# 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:

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:

# 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.