parsaaghayi-backend/vendor/darkaonline/l5-swagger/.claude/skills/write-tests.md
parsa aghaei 640bd67556
Some checks failed
Deploy Backend / deploy (push) Failing after 1m13s
chore: commit vendor for offline build
2026-06-28 16:47:17 +03:30

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: 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).

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:

  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

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