parsaaghayi-backend/vendor/darkaonline/l5-swagger/.claude/skills/write-tests.md
DevOps c9a688e80d
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
chore: commit vendor directory for fully offline builds
2026-06-28 18:08:40 +00:00

274 lines
9.9 KiB
Markdown

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