Guides

Tests and CI

Give each layer its own test suite, run one layer's tests, and fail pull requests that cross a boundary.

Each layer keeps its tests in its own tests/ folder, under its own namespace. osdd:phpunit turns them into one PHPUnit suite per layer, and osdd:check guards the boundaries in CI.

Write tests in a layer

php artisan osdd:test PlaceOrderTest --layer=functional/orders
php artisan osdd:test OrderTotalTest --unit --layer=functional/orders
FolderNamespace
functional/orders/tests/FeatureFunctional\Orders\Tests\Feature
functional/orders/tests/UnitFunctional\Orders\Tests\Unit

A test may use the layers in the layer's require, plus the ones in its require-dev. Put test-only helpers in a layer such as technical/testing and require it in require-dev.

One suite per layer

php artisan osdd:phpunit

The command adds a <testsuite> named after each layer to phpunit.xml, adds each layer's source directory to the coverage, and excludes the layer migrations from it. Run it again after creating a layer. See osdd:phpunit for the exact entries.

php artisan test --testsuite=functional/orders   # one layer
php artisan test                                 # everything

Seed in tests

RefreshDatabase with $seed = true runs the layer seeders once LayersSeeder is the default seeder, which osdd:start sets up. Otherwise name it:

use Illuminate\Foundation\Testing\RefreshDatabase;
use Lomkit\Osdd\Seeders\LayersSeeder;

class PlaceOrderTest extends TestCase
{
    use RefreshDatabase;

    protected bool $seed = true;

    protected string $seeder = LayersSeeder::class;
}

Check the boundaries on every pull request

Run osdd:check after composer install, from the root of the Laravel project. With --format=github, each violation becomes an annotation on the pull request's "Files changed" tab, and the step fails with exit code 1:

.github/workflows/tests.yml
name: tests

on: [push, pull_request]

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Prepare the application
        run: cp .env.example .env && php artisan key:generate

      - name: Check layer boundaries
        run: php artisan osdd:check --format=github

      - name: Run tests
        run: php artisan test
::error file=functional/orders/src/Checkout.php,line=7,title=Undeclared layer dependency::functional/orders uses Functional\Billing\Models\Invoice from functional/billing but does not require functional/billing.

The annotation paths are relative to the Laravel root: when the application lives in a sub-folder of the repository, set working-directory on the step. Other CI systems can read --format=json; see osdd:check.

Unused requirements are warnings: they show as annotations but do not fail the step.