--- name: write-tests description: Write PHPUnit tests for the L5-Swagger Laravel package using Orchestra Testbench, following project conventions for mocking, config manipulation, and PHPUnit attributes allowed-tools: Read, Edit, Write, Bash(vendor/bin/phpunit *), Bash(composer run-script phpunit), Bash(composer run-script analyse), Bash(grep *), Bash(find *), Bash(ls *), Bash(diff *), Bash(cat *) --- # Write Tests for L5-Swagger Generate PHPUnit tests for the L5-Swagger package following its established patterns. ## Context - Test runner: !`composer run-script phpunit -- --version 2>/dev/null | head -1` - Existing test files: !`ls tests/Unit/*.php 2>/dev/null | xargs -I{} basename {}` - Source files: !`ls src/*.php src/**/*.php 2>/dev/null | grep -v vendor` ## Project Test Architecture All tests extend `Tests\Unit\TestCase` which extends Orchestra Testbench's `OrchestraTestCase`. This gives every test a full Laravel application with the `L5SwaggerServiceProvider` registered. ### Base TestCase provides: | Property / Method | Purpose | |---|---| | `$this->configFactory` | `ConfigFactory` instance resolved from the app container | | `$this->generator` | `Generator` instance resolved from the app container | | `$this->fileSystem` | PHPUnit mock of `Illuminate\Filesystem\Filesystem` (created via `createMock`) | | `setAnnotationsPath()` | Points config to `tests/storage/annotations/OpenApi/` fixtures, enables `generate_always` and `generate_yaml_copy`, sets `L5_SWAGGER_CONST_HOST` constant, rebuilds the generator | | `makeGeneratorWithMockedFileSystem()` | Injects `$this->fileSystem` mock into the generator via reflection | | `setCustomDocsFileName($name, $type)` | Overrides docs filename in config for JSON or YAML | | `crateJsonDocumentationFile()` | Creates a minimal `{}` JSON docs file | | `createYamlDocumentationFile()` | Creates an empty YAML docs file | | `jsonDocsFile()` | Returns absolute path to the JSON docs file (creates dir if needed) | | `yamlDocsFile()` | Returns absolute path to the YAML docs file (creates dir if needed) | | `copyAssets()` | Copies swagger-ui dist into testbench vendor dir (runs in setUp) | | `deleteAssets()` | Removes the copied swagger-ui assets | ### tearDown cleanup The base TestCase automatically deletes generated JSON/YAML docs files and the docs directory in `tearDown()`. You do NOT need to clean up generated files. ## Instructions ### Step 1: Determine what to test Read the source file(s) the user wants tested. Identify: - Public methods and their behavior - Error/exception paths - Config-driven behavior branches - Interactions with the filesystem or external dependencies ### Step 2: Create or edit the test file **File location**: `tests/Unit/{ClassName}Test.php` **Required class-level attributes** (PHPUnit 11 attributes, not annotations): ```php #[TestDox('Human readable class description')] #[CoversClass(FullyQualifiedClassName::class)] ``` **Required test method conventions**: - Method names: `testItDoesX` or `testCanDoX` (camelCase, descriptive) - Visibility: `public function testXxx(): void` - Add `@throws` docblock for expected exceptions - Use `expectException()` and `expectExceptionMessage()` for exception tests ### Step 3: Follow these patterns based on what you're testing #### Pattern A: Testing Generator behavior with mocked filesystem Use when testing `Generator` methods that interact with the filesystem (directory creation, file writing, permission checks). ```php public function testItThrowsExceptionIfSomethingFails(): void { $this->setAnnotationsPath(); $config = $this->configFactory->documentationConfig(); $docs = $config['paths']['docs']; // Set up filesystem mock expectations $this->fileSystem ->expects($this->once()) ->method('exists') ->with($docs) ->willReturn(true); // ... more mock setup ... $this->expectException(L5SwaggerException::class); $this->expectExceptionMessage('Expected error message'); // IMPORTANT: call makeGeneratorWithMockedFileSystem() AFTER mock setup $this->makeGeneratorWithMockedFileSystem(); $this->generator->generateDocs(); } ``` #### Pattern B: Testing full generation pipeline (integration) Use when testing that docs generate correctly with specific config. ```php public function testCanGenerateWithSpecificConfig(): void { $this->setAnnotationsPath(); // Optionally override config $cfg = config('l5-swagger.documentations.default'); $cfg['paths']['base'] = 'https://custom-server.url'; config(['l5-swagger' => [ 'default' => 'default', 'documentations' => ['default' => $cfg], 'defaults' => config('l5-swagger.defaults'), ]]); $this->generator->generateDocs(); $this->assertFileExists($this->jsonDocsFile()); // Verify via HTTP response $this->get(route('l5-swagger.default.docs')) ->assertSee('expected content') ->assertStatus(200); } ``` #### Pattern C: Testing routes and HTTP responses Use when testing controller behavior, middleware, or route registration. ```php public function testRouteReturnsExpectedResponse(): void { // For tests needing generated docs, call setAnnotationsPath() first // For tests checking behavior without docs, don't call it $this->get(route('l5-swagger.default.docs')) ->assertStatus(200) ->assertSee('expected') ->assertHeader('Content-Type', 'application/json'); } ``` #### Pattern D: Testing config merging Use when testing `ConfigFactory` behavior. ```php #[DataProvider('configDataProvider')] public function testConfigMergesCorrectly(array $data, array $expected): void { config(['l5-swagger' => array_merge($data, [ 'defaults' => [/* base defaults */], ])]); $config = $this->configFactory->documentationConfig(); $this->assertSame($expected['key'], $config['key']); } public static function configDataProvider(): \Generator { yield 'descriptive case name' => [ 'data' => [/* input */], 'expected' => [/* expected output */], ]; } ``` #### Pattern E: Mocking the Generator via GeneratorFactory Use when testing controllers/routes that call `generateDocs()` and you want to control generator behavior without actual generation. ```php public function testBehaviorWhenGenerationFails(): void { $mockGenerator = $this->createMock(Generator::class); $mockGeneratorFactory = $this->createMock(GeneratorFactory::class); $mockGeneratorFactory->method('make')->willReturn($mockGenerator); app()->extend(GeneratorFactory::class, function () use ($mockGeneratorFactory) { return $mockGeneratorFactory; }); $mockGenerator->expects($this->once()) ->method('generateDocs') ->willThrowException(new L5SwaggerException()); $this->get(route('l5-swagger.default.docs'))->assertNotFound(); } ``` ### Step 4: Config manipulation pattern When overriding config, always preserve the full structure: ```php config(['l5-swagger' => [ 'default' => 'default', 'documentations' => [ 'default' => $cfg, // your modified config ], 'defaults' => config('l5-swagger.defaults'), ]]); ``` After changing config that affects the generator, call `$this->makeGenerator()` to rebuild it. ### Step 5: Run and verify with coverage gate New tests must never decrease code coverage. Follow this sequence: #### 5a. Capture baseline coverage BEFORE writing tests ```bash vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-before.txt ``` Extract the baseline percentages: ```bash grep -E 'Lines:|Methods:|Classes:' /tmp/l5-coverage-before.txt ``` Record both **Lines** and **Methods** percentages — these are the numbers that must not drop. #### 5b. Run the new/modified tests in isolation ```bash vendor/bin/phpunit tests/Unit/YourNewTest.php --testdox ``` #### 5c. Run the full suite with coverage AFTER adding tests ```bash vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-after.txt ``` #### 5d. Compare coverage ```bash diff /tmp/l5-coverage-before.txt /tmp/l5-coverage-after.txt ``` Verify: - **Lines coverage**: must be >= baseline - **Methods coverage**: must be >= baseline If coverage decreased, identify the cause: 1. A new test file with `#[CoversClass]` pulled in a class that was previously uncovered — add tests for the uncovered methods 2. A test is covering code paths that were already covered but skipping others — add assertions for the missing branches 3. A new fixture or helper class landed under `src/` — it needs its own tests **Do not proceed until coverage is equal to or higher than the baseline.** #### 5e. Run static analysis ```bash composer run-script analyse ``` ## Important Rules - **Coverage must not decrease.** Always capture baseline coverage before writing tests and verify it afterwards. If a `#[CoversClass]` attribute pulls in a class with uncovered methods, you must add tests for those methods too — not just remove the attribute. This is a hard gate: do not report the task as complete until coverage is verified equal or higher. - Use PHP 8.2+ features: constructor property promotion, union types, named arguments, match expressions - Use PHPUnit 11 **attributes** (`#[TestDox]`, `#[CoversClass]`, `#[DataProvider]`), NOT docblock annotations (`@testdox`, `@covers`, `@dataProvider`) - Data providers must be `public static` methods returning `\Generator` (using `yield`) - Fixture annotations live in `tests/storage/annotations/OpenApi/` — read existing ones before creating new fixtures - The `$this->fileSystem` mock is created fresh via `#[Before]` attribute before each test — no shared mock state between tests - Route names follow the pattern `l5-swagger.{documentation}.{type}` where type is `api`, `docs`, `asset`, or `oauth2_callback` - The test environment sets `SWAGGER_VERSION=3.0` and `APP_KEY` via `phpunit.xml` - StyleCI enforces Laravel preset — don't fight the code style