Skip to content

Specific instructions/abilities or tool for testing #662

@BigBlockStudios

Description

@BigBlockStudios

AI models seem to do fairly well at writing tests but not designing tests - I frequently end up writing a separate instruction set specific to testing and including it as context.

It would be nice to see boost including include an instruction set specific to designing Laravel tests using accepted best practises and methodology.

No - this is not Laravel/Boost specific, but it would be nice to not have to find, refactor, write a specific instruction set for every Laravel project I work on.

  • generally AI does not use a coverage report to identify what needs to be tested or is already tested elsewhere (or how to interpret that report)
  • AI generates a lot of spurious tests (excessive token use)
  • AI generally does not stop on failure, or target specific test classes/methods (more excessive token use)

something along the lines of:

---
applyTo: "tests/**"
---

# PHPUnit Test Design Methodology

## Test Philosophy

Tests verify behaviour, not implementation. Before writing any test, ask: *if the implementation changed but the outcome stayed the same, would this test still pass?* It should.

Tests must be **FIRST**: Fast, Isolated, Repeatable, Self-validating, Timely. Each test method verifies one thing. A failing test should identify the broken behaviour without ambiguity.

Structure every test using **AAA**: arrange preconditions, act on the SUT, assert the expected outcome — separated by blank lines.

---

## What to Test

For any feature, cover:

- Happy path — expected output, side effects, response status
- Unauthenticated access — redirect or 401
- Unauthorised access — 403, tested against the policy/gate directly, not through a controller that has other logic
- Each required field missing
- Each boundary violation (min, max, type)
- Uniqueness and foreign key constraints — tested behaviourally via HTTP, not by asserting rule strings
- Edge cases — null optionals, empty collections, boundary values

**Behavioural over structural.** Prefer submitting invalid input through the stack and asserting the error, over asserting that a rules array contains a particular string. Structural assertions are only acceptable for methods that cannot be reached via HTTP; document why.

**One assertion purpose per test.** Redundant assertions — checking status 200 before `assertSee`, asserting a value that a prior assertion already implies — add noise without signal. Remove them.

---

## Test Isolation

- Prefer private factory methods over `setUp()` for building the SUT. `setUp()` runs for every test and its allocated objects persist in memory until the suite completes; factory methods use locals that are freed immediately.
- Authorization tests: test policies directly on the policy class, or via a minimal dedicated test route. Do not test authorization by routing through a real controller — a 403 could come from middleware, the policy, or a manual `abort()`, and you cannot tell which.
- **Flag gaps before writing tests.** Before writing tests for any class, audit it for discrepancies between intent and implementation:
  - *Empty stub methods* — a method with no body (or just `//`) has no testable behaviour. Do not write a test for it; instead, add a comment in the test file: `// NOTE: MethodName() is an unimplemented stub — no tests written until it has behaviour.`
  - *Authorization gaps* — if a policy exists for the model but the controller action does not invoke `$this->authorize()`, the effective rule is "any authenticated user", which may be unintentional. Write tests reflecting actual behaviour and note: `// NOTE: FooPolicy::view() exists but action does not call $this->authorize().`
  - *Other gaps* — missing validation on a mutating action, a relationship loaded in one branch but not another, a response that returns inconsistent status codes. Flag anything that looks like it contradicts the declared design.
  
  Report all flagged gaps to the user before or alongside delivering the tests. Do not silently paper over them.
- Use `#[CoversClass(ClassName::class)]` on every test class. This enforces test boundaries, prevents accidental coverage, and scopes `--covers` filtering.
- Use `#[Small]`, `#[Medium]`, `#[Large]` to categorise by scope and execution time.
- Use data providers (`#[DataProvider]` or `#[TestWith]`) for boundary tests rather than repeating near-identical test methods.

---

## Running Tests

These rules extend and take precedence over any running-tests guidance in `copilot-instructions.md`. Prefer targeted execution to keep feedback fast and context compact.

Run a specific file:

php artisan test --compact tests/Feature/Http/Requests/StoreNominationRequestTest.php

Filter by test class or method name:

php artisan test --compact --filter StoreNominationRequestTest
php artisan test --compact --filter test_label_too_short


- Use `--filter` with a test class name or method name for the most immediate targeting.
- Only run the full suite (`php artisan test --compact`) to confirm no regressions after a feature is complete.

---

## Coverage-Guided Testing

Coverage reports show which lines executed — not whether behaviour was verified. A fully green report can hide a suite full of hollow assertions.

Use coverage to find untested **branches** (the `false` side of a condition, an unhandled exception path, a guard clause never triggered), not to reach a line-count target. For each uncovered branch, identify what input or state would cause it to execute, then write a test that creates that state and asserts the resulting outcome.

Coverage at 80% with meaningful assertions is more valuable than 100% with hollow ones.

When using a coverage report as the starting point, generate it first, sort by lowest branch coverage, prioritise critical paths (authorisation, mutation, financial logic), then author tests. Do not author tests bottom-up from the report — use it as a gap-finder, then design tests from the user/behaviour perspective.

---

## Naming

Test method names are executable specifications. They should read as a sentence:

- `test_unauthenticated_request_redirects_to_login`
- `test_non_admin_user_is_forbidden`
- `test_label_too_short_fails_validation`
- `test_valid_payload_creates_record_and_returns_201`

Avoid `test_store`, `test_it_works`, `test_validation`. The name should describe the scenario and expected outcome, not the method being called.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions