codeception/specify
Trait for BDD-style specifications in PHPUnit/Codeception. Lets you write tests with describe/it-like blocks, shared setup, and clear expectations, improving readability while staying compatible with standard PHP unit testing workflows.
## Getting Started
### Minimal Setup
1. **Installation**
```bash
composer require --dev codeception/specify:^2.0
Basic Usage Import the trait in your test class:
use Codeception\Specify;
class MyTest extends \Codeception\Test\Unit
{
use Specify;
}
First Use Case
Replace traditional assert blocks with specify():
public function testUserCreation()
{
$user = User::create(['name' => 'John']);
specify('the user has a valid name', function () use ($user) {
$user->name->shouldBe('John');
});
specify('the user is saved', function () use ($user) {
User::where('id', $user->id)->shouldHaveCount(1);
});
}
tests/ in the package repo for real-world usage.Descriptive Test Blocks
Replace verbose assert chains with readable specify() blocks (unchanged):
public function testOrderProcessing()
{
$order = Order::create(['status' => 'pending']);
specify('order starts as pending', function () use ($order) {
$order->status->shouldBe('pending');
});
$order->markAsPaid();
specify('order updates to paid', function () use ($order) {
$order->refresh()->status->shouldBe('paid');
});
}
Data-Driven Specifications
Use specify() with loops or factories (unchanged):
public function testUserRoles()
{
$roles = ['admin', 'editor', 'guest'];
foreach ($roles as $role) {
specify("a $role has correct permissions", function () use ($role) {
$user = User::factory()->create(['role' => $role]);
$user->can($role)->shouldBeTrue();
});
}
}
Integration with Codeception Works seamlessly with Codeception’s actors (unchanged):
class UserCest
{
use Specify;
public function _before(Actor $I)
{
$I->wantTo('test user workflow');
}
public function testLogin(Actor $I)
{
$I->amOnPage('/login');
specify('login form loads', function () use ($I) {
$I->see('Email');
});
}
}
Combining with PHPUnit
Mix specify() with PHPUnit’s @dataProvider (unchanged):
public function testEdgeCases()
{
$this->specifyDataProvider('edgeCaseProvider', function ($input, $expected) {
$result = process($input);
$result->shouldBe($expected);
});
}
Laravel\LaravelTestCase for HTTP tests (unchanged):
use Laravel\LaravelTestCase;
use Codeception\Specify;
class FeatureTest extends LaravelTestCase
{
use Specify;
}
Specify for domain-specific specs (unchanged):
trait CustomSpecify
{
public function specifyPaymentIsValid($payment)
{
specify('amount is positive', function () use ($payment) {
$payment->amount->shouldBeGreaterThan(0);
});
}
}
specify() closures:
specify('user type is correct', function () use ($user) {
match ($user->role) {
'admin' => true,
default => false,
}->shouldBeTrue();
});
PHP Version Compatibility
PHP version mismatch or undefined method errors.composer.json:
"config": {
"platform": {
"php": "7.4"
}
}
Scope Leaks (unchanged)
Avoid referencing outer variables without use:
// ❌ Fails
specify('user exists', function () {
$user->id; // Error!
});
// ✅ Correct
specify('user exists', function () use ($user) {
$user->id;
});
Overusing specify() (unchanged)
// ✅ Grouped
specify('user validation', function () use ($user) {
$user->validate()->shouldBeEmpty();
$user->errors->shouldBeEmpty();
});
// ❌ Overly granular
specify('no validation errors', function () use ($user) {
$user->validate()->shouldBeEmpty();
});
specify('errors array is empty', function () use ($user) {
$user->errors->shouldBeEmpty();
});
Codeception-Specific Quirks (unchanged)
ActorTestCase closures access $I via use ($I).Failed Specifications
specify() block description in failure messages:
1) Tests\UserTest::testCreation
specify 'user is saved' failed: User with id [1] not found.
PHP 8.1 Strict Typing
strict_types=1 in test files for type safety:
declare(strict_types=1);
public function testStrictTypes()
{
specify('integer input', function () {
(1)->shouldBe(1); // Passes
("1")->shouldBe(1); // Fails in strict mode
});
}
CI/CD Pipeline
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
Naming Conventions (unchanged) Use imperative mood:
Dynamic Specifications (unchanged) Generate blocks dynamically:
$testCases = collect([1, 2, 3]);
$testCases->each(function ($input) {
specify("processes $input correctly", function () use ($input) {
process($input)->shouldBe($input * 2);
});
});
Extending Functionality (unchanged)
Add custom specify* methods:
trait ExtendedSpecify
{
public function specifyModelHasAttribute($model, string $attribute, $value)
{
specify("model has $attribute=$value", function () use ($model, $attribute, $value) {
$model->$attribute->shouldBe($value);
});
}
}
Legacy Code Migration
assert* and specify() during transition:
public function testHybrid()
{
assertTrue(true); // Legacy
specify('new spec style', function () {
true->shouldBeTrue();
});
}
phpstan or psalm to detect deprecated patterns post-migration.PHP 8.1 Performance
; php.ini
opcache.jit_buffer_size=100M
opcache.jit=tracing
Xdebug to identify bottlenecks in `specifyHow can I help you explore Laravel packages today?