Installation
composer require innmind/black-box
Add to composer.json under require-dev if only for testing:
"dev-require": {
"innmind/black-box": "^1.0"
}
First Test File
Create a test file (e.g., tests/Feature/PropertyTest.php):
use Innmind\BlackBox\{Application, Set, Runner\Assert, Prove};
use Tests\TestCase;
class PropertyTest extends TestCase
{
public function testAddCommutativity()
{
Application::new([])
->tryToProve(static function(Prove $prove) {
yield $prove
->proof('add is commutative')
->given(Set::integers())
->test(static fn(Assert $assert, int $a, int $b) =>
$assert->same($a + $b, $b + $a)
);
});
}
}
Run Tests
php artisan test
Or for a single test:
php artisan test tests/Feature/PropertyTest.php
Use BlackBox to verify invariants in your Laravel application, such as:
Example: Testing a discount calculation:
$prove
->proof('discounts are applied correctly')
->given(Set::floats(0, 100), Set::floats(0, 1)) // (price, discountRate)
->test(static fn(Assert $assert, float $price, float $discountRate) =>
$assert->same(
applyDiscount($price, $discountRate),
$price * (1 - $discountRate)
)
);
Leverage Set to generate diverse inputs for testing:
Set::integers(1, 100) // Random integers between 1-100
Set::floats(0, 100) // Random floats
Set::strings(10) // Random strings of length 10
Set::of(static fn() => User::factory()->make())
Set::of(static fn() => collect([1, 2, 3]))
Group related properties in a single test:
Application::new([])
->tryToProve(static function(Prove $prove) {
yield $prove
->proof('user email is valid')
->given(Set::strings(5, 50))
->test(static fn(Assert $assert, string $email) =>
$assert->true(Filter::validate($email))
);
yield $prove
->proof('user email is unique')
->given(Set::of(static fn() => User::factory()->make()))
->test(static fn(Assert $assert, User $user) =>
$assert->false(User::where('email', $user->email)->exists())
);
});
Use Set::of() with Laravel’s factories to test domain logic:
use App\Models\User;
$prove
->proof('user roles are enforced')
->given(Set::of(static fn() => User::factory()->state([
'role' => Set::elements(['admin', 'editor', 'viewer'])
])))
->test(static fn(Assert $assert, User $user) =>
$assert->same(
canAccessDashboard($user),
$user->role === 'admin'
)
);
Override default shrinkers (e.g., for complex objects) to improve failure debugging:
use Innmind\BlackBox\Shrinker\Shrinker;
$customShrinker = new class implements Shrinker {
public function shrink(mixed $value): iterable { /* ... */ }
};
Application::new([], [$customShrinker])
->tryToProve(...);
Run proofs in parallel for faster execution (Laravel 9+):
use Innmind\BlackBox\Runner\Parallel;
Application::new([], [], new Parallel(4)) // 4 parallel workers
->tryToProve(...);
Stateful Tests
BlackBox is designed for stateless properties. Avoid testing:
DatabaseTransactions trait instead).Mockery or Pest).❌ Anti-pattern:
// Fails: Assumes a clean DB state.
$prove->test(static fn(Assert $assert) =>
$assert->same(User::count(), 0)
);
Overly Complex Generators
Generators with side effects (e.g., Set::of(static fn() => tap(new User(), fn($u) => $u->save()))) can bloat tests. Prefer pure functions.
Ignoring Shrinking Always review shrunk values when a test fails—they reveal the minimal input that broke your property.
Performance with Large Sets Limit generator size for slow operations:
Set::integers(1, 1000)->size(100) // Test only 100 samples
Inspect Generated Values Log inputs during failures:
->test(static fn(Assert $assert, int $a, int $b) => {
logger()->debug("Failed with: a=$a, b=$b");
$assert->same($a + $b, $b + $a);
})
Custom Assertions
Extend Assert for domain-specific checks:
$assert->custom('is_valid_order', static fn(Order $order) =>
$order->total > 0 && $order->items->count() > 0
);
Focused Testing
Use Application::new([], [], null, ['proof_name']) to run a single proof:
Application::new([], [], null, ['add is commutative'])
->tryToProve(...);
Custom Runners
Implement Runner\RunnerInterface to integrate with Laravel’s test events:
use Innmind\BlackBox\Runner\RunnerInterface;
class LaravelRunner implements RunnerInterface {
public function run(Proof $proof): void {
// Dispatch Laravel events (e.g., testingStarted, testingFinished)
}
}
Plugin System
Extend Application with plugins for Laravel-specific features:
Application::new([], [], null, [], [
new class {
public function __invoke(Application $app) {
$app->setDatabaseTransactions(true);
}
}
]);
Type-Safe Generators Use PHP 8.1+ attributes to validate generators:
#[Assert\All(Assert\Type::class, 'int')]
Set::integers();
Service Container Binding
Bind BlackBox components to Laravel’s container for DI:
$this->app->bind(Prove::class, static fn() => new Prove());
Artisan Integration Create a custom Artisan command for ad-hoc property testing:
php artisan blackbox:test --proof="add is associative"
Pest Plugin Integrate with Pest for a more fluent syntax:
use Innmind\BlackBox\{Application, Set};
test('discounts are applied', function () {
Application::new([])
->tryToProve(static function(Prove $prove) {
yield $prove->proof('discount logic')
->given(Set::floats(0, 100))
->test(static fn(Assert $assert, float $price) =>
$assert->greaterThan(applyDiscount($price, 0.1), 0)
);
});
});
How can I help you explore Laravel packages today?