memran / php-testify
Expressive PHP testing on top of PHPUnit with expect() assertions, describe()/it() specs, and watch mode.
Requires
- php: ^8.2
- phpunit/phpunit: ^11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.76
- phpstan/phpstan: ^2.1
README
PHP-Testify brings an expressive expect() API, describe() / it() specs, a readable CLI runner, and watch mode to standard PHPUnit-based projects. It stays small, framework-agnostic, and compatible with plain Composer workflows.
Features
- Fluent assertions such as
toBe(),toEqual(),toContain(),toThrow(), and negation withnot() - Parameterized specs with
->with([...]) - Fluent skip/incomplete controls with
->skip(),incomplete(), andtodo() - Fluent groups/tags with
->group()/group()plus CLI filtering - Two test styles in the same project: PHPUnit classes and Jest/Vitest-like specs
- Built-in lifecycle hooks with nested-suite inheritance:
beforeAll,afterAll,beforeEach,afterEach - Filtered runs, grouped runs, verbose output, and watch mode from the
bin/testifyCLI - Static analysis, formatting, and CI setup ready for package contributors
Requirements
- PHP 8.2 or newer
- Composer
Installation
composer require --dev memran/php-testify
Create a phpunit.config.php file in your project root:
<?php return [ 'bootstrap' => __DIR__ . '/vendor/autoload.php', 'test_patterns' => [ __DIR__ . '/tests/*Test.php', __DIR__ . '/tests/*_test.php', ], ];
Quick Start
Write your first spec:
<?php declare(strict_types=1); use function Testify\describe; use function Testify\expect; use function Testify\it; describe('cart totals', function (): void { it('adds line items', function (): void { $items = [12, 8, 5]; expect(array_sum($items))->toBe(25); expect($items)->toHaveLength(3); }); });
Run it:
php bin/testify
Parameterized and grouped specs:
<?php declare(strict_types=1); use function Testify\describe; use function Testify\expect; use function Testify\it; describe('cart totals', function (): void { it('adds line items', function (array $items, int $expected): void { expect(array_sum($items))->toBe($expected); })->with([ 'two items' => [[12, 8], 20], 'three items' => [[12, 8, 5], 25], ])->group('cart', 'fast'); })->group('unit');
Tutorials
1. Write a spec with hooks
<?php declare(strict_types=1); use function Testify\beforeEach; use function Testify\describe; use function Testify\expect; use function Testify\it; describe('account state', function (): void { $balance = 0; beforeEach(function () use (&$balance): void { $balance = 100; }); it('withdraws funds', function () use (&$balance): void { $balance -= 40; expect($balance)->toBe(60); expect($balance)->not()->toBe(100); }); });
2. Mix PHPUnit and Testify in one project
tests/InvoiceCalculatorTest.php
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; final class InvoiceCalculatorTest extends TestCase { public function testRoundsTaxAmount(): void { self::assertSame(13, (int) round(12.6)); } }
tests/invoice_expectations_test.php
<?php declare(strict_types=1); use function Testify\describe; use function Testify\expect; use function Testify\it; describe('invoice presentation', function (): void { it('contains a currency symbol', function (): void { expect('$12.50')->toContain('$'); }); });
3. Run focused feedback loops
php bin/testify --filter invoice php bin/testify --group api php bin/testify --group api --exclude-group slow php bin/testify --verbose php bin/testify --watch
Use --filter to run matching PHPUnit methods, spec names, suite names, or dataset labels. Use --group and --exclude-group to select fluent tests by tags. Use --watch during local development to re-run the suite after file changes without shell interpolation.
4. Skip or mark work incomplete
<?php declare(strict_types=1); use function Testify\describe; use function Testify\incomplete; use function Testify\it; use function Testify\skip; describe('feature rollout', function (): void { it('ships later', function (): void { })->skip('waiting for backend support'); it('needs more implementation', function (): void { incomplete('validation rules still missing'); }); it('is planned work', function (): void { todo('queued for next sprint'); }); });
Assertion Examples
expect($value)->toBe('exact'); expect($value)->toEqual(['loose' => 'match']); expect($value)->toBeTruthy(); expect($value)->toBeGreaterThan(10); expect($value)->toBeGreaterThanOrEqual(10); expect($array)->toContain('item'); expect($array)->toHaveCount(3); expect($array)->toHaveKey('name'); expect($array)->toHaveKeyWithValue('role', 'admin'); expect('php-testify')->toStartWith('php'); expect('php-testify')->toEndWith('fy'); expect('php-testify')->toMatch('/test/i'); expect(3.14159)->toBeCloseTo(3.14, 0.01); expect($callable)->toThrow(RuntimeException::class); expect($callable)->toThrowWithMessage(RuntimeException::class, 'boom'); expect($callable)->toThrowWithCode(RuntimeException::class, 42); expect($object)->toBeInstanceOf(User::class); expect($object)->not()->toBeSameObject($otherObject);
Project Layout
src/runtime classes, assertions, suite registry, runner, and watch supportbin/testifyCLI entrypointtests/package fixtures plus PHPUnit coverage for internal behaviorphpunit.config.phpfixture config used by the package integration suite
Development
composer install
composer test
composer test:package
composer analyse
composer lint
composer fix
composer ci
composer testruns the internal PHPUnit suite fromtests/Unitcomposer test:packageruns the package through its own CLI against fixture specscomposer analyseruns PHPStancomposer lintruns PHP-CS-Fixer in dry-run modecomposer fixapplies formatting fixescomposer ciruns the full local quality gate
Tooling Status
- PHPStan 2.x configuration: phpstan.neon.dist
- PHPUnit configuration: phpunit.xml.dist
- GitHub Actions workflow: .github/workflows/ci.yml
- Contributor guide: AGENTS.md
Security and Production Notes
- Watch mode spawns child processes with argument arrays, not shell-built command strings.
- The runner validates
bootstrapandtest_patternsbefore loading files. - Keep
phpunit.config.phpunder version control and point it only at trusted bootstrap files. - Fluent groups/tags currently apply to Testify specs, not native PHPUnit
#[Group]attributes.
Supported Today
describe()/it()/test()specs- nested suites with inherited
beforeEach/afterEachhooks - datasets with
->with([...]) - fluent groups/tags with CLI selection
- skip/incomplete states through metadata or runtime helpers
- mixed projects that contain both PHPUnit test classes and Testify specs
- expanded day-to-day matchers in
expect()
Current Limits
- PHPUnit attributes such as
#[DataProvider],#[Depends], and#[Group]are not re-exposed through the Testify runner yet - fluent datasets are inline per test; reusable named dataset registries are not implemented
- there is no XML bridge for
phpunit.xmlorphpunit.xml.dist;phpunit.config.phpremains the runtime config source - machine-readable reporters and advanced PHPUnit extension/event integration are still missing
Contributing
Run composer ci before opening a pull request. Keep changes focused, add regression tests for behavior changes, and document any CLI or configuration changes in this README.