9.9 KiB
| name | description | allowed-tools |
|---|---|---|
| write-tests | Write PHPUnit tests for the L5-Swagger Laravel package using Orchestra Testbench, following project conventions for mocking, config manipulation, and PHPUnit attributes | 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):
#[TestDox('Human readable class description')]
#[CoversClass(FullyQualifiedClassName::class)]
Required test method conventions:
- Method names:
testItDoesXortestCanDoX(camelCase, descriptive) - Visibility:
public function testXxx(): void - Add
@throwsdocblock for expected exceptions - Use
expectException()andexpectExceptionMessage()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).
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.
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.
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.
#[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.
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:
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
vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-before.txt
Extract the baseline percentages:
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
vendor/bin/phpunit tests/Unit/YourNewTest.php --testdox
5c. Run the full suite with coverage AFTER adding tests
vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-after.txt
5d. Compare coverage
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:
- A new test file with
#[CoversClass]pulled in a class that was previously uncovered — add tests for the uncovered methods - A test is covering code paths that were already covered but skipping others — add assertions for the missing branches
- 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
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 staticmethods returning\Generator(usingyield) - Fixture annotations live in
tests/storage/annotations/OpenApi/— read existing ones before creating new fixtures - The
$this->fileSystemmock 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 isapi,docs,asset, oroauth2_callback - The test environment sets
SWAGGER_VERSION=3.0andAPP_KEYviaphpunit.xml - StyleCI enforces Laravel preset — don't fight the code style