NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Testing for Laravel

For applications

A plain Laravel app — no Roundly package, no base-class change — gets the app-usable subset through the Pest expectations or the static Assert, straight from its existing suite:

use RoundlyConsulting\Testing\Assert;

it('has a runnable migration order', function (): void {
    expect(database_path('migrations'))->toHaveRunnableMigrationOrder();
});

it('reads every services key it relies on from the shipped config', function (): void {
    // Forward-only: an app's config legitimately carries keys read by vendor packages.
    expect(config_path('services.php'))->toSatisfyConfigContract(app_path(), ['reverse' => false]);
});

it('does not leak credentials through artisan about', function (): void {
    // Only a secret that is set can be looked for — a null or empty entry throws.
    config(['services.stripe.secret' => 'sk_test_do_not_render_me']);

    expect('environment')->toLeakNoSecrets(
        secrets: [config('app.key'), config('services.stripe.secret')],
        mustRender: ['Application Name'],
    );
});

What applies to apps

  • The migration-order pin, the real-engine runner and its negative control.
  • The config contract in forward-only mode — reverse is opt-in.
  • The secret-safe about capture.
  • The model-swap proof — apps consume config-swappable vendor models too, so you can prove a vendor package really listens.
  • The lock recorders and DriverMatrix.
  • Every architecture preset. runtimeRequireIsWhitelisted encodes the Roundly whitelist, but $alsoAllow makes even that usable.

Architecture presets in an app

noDebuggingLeftovers scans <cwd>/src by default, so point it at app/ in an application:

<?php

declare(strict_types=1);

use RoundlyConsulting\Testing\Arch\ArchPresets;

// tests/ArchTest.php in a Laravel application
ArchPresets::strictTypes('App');
ArchPresets::noDebuggingLeftovers(srcDir: __DIR__.'/../app'); // the default scans <cwd>/src

Plain PHPUnit: the static Assert

Every expectation has a static mirror on RoundlyConsulting\Testing\Assert for class-based tests — toBeSwappableVia and the facade contract included. Both delegate to the same implementation, so the check is identical however you reach it:

Pest expectationStatic mirror
toHaveRunnableMigrationOrderAssert::migrationsRunInDependencyOrder($dir, $expectedForeignKeys, $tableResolvers, $externalTables)
toApplyOnConnectionAssert::migrationsApplyOnConnection($dir, $connection, $expectedMigrations)
toRejectBrokenOrderOnConnectionAssert::brokenOrderIsRejectedOnConnection($dir, $reorder, $connection)
toNotAutoLoadMigrationsAssert::doesNotAutoLoadMigrations($providerClass, $migrationsDir)
toPublishMigrationsTimestampedAssert::publishesMigrationsTimestamped($providerClass, $tag, $count)
toSatisfyConfigContractAssert::configContract($configPath, $srcDirs, $prefix, $options)
toLeakNoSecretsAssert::aboutSectionLeaksNoSecrets($section, $secrets, $mustRender)
toHonourModelSwapAssert::modelSwapHonoured($configKey, $subclass, $exercise, $expectsCreation)
toBeSwappableViaAssert::modelIsSwappableVia($model, $configKey)
toDocumentItsRootAssert::facadeDocumentsItsRoot($facade, $except)
toBeFakeableAssert::facadeIsFakeable($facade)
toReachEveryActionAssert::facadeReachesEveryAction($facade, $actionsDir, $except, $via)
use PHPUnit\Framework\Attributes\Test;
use RoundlyConsulting\Testing\Assert;
use Tests\TestCase;

final class SchemaTest extends TestCase
{
    #[Test]
    public function migrations_run_in_dependency_order(): void
    {
        Assert::migrationsRunInDependencyOrder(database_path('migrations'));
    }

    #[Test]
    public function the_services_config_ships_every_key_the_app_reads(): void
    {
        Assert::configContract(config_path('services.php'), app_path(), options: ['reverse' => false]);
    }
}

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.