PHPackages                             k2gl/phpunit-fluent-assertions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. k2gl/phpunit-fluent-assertions

ActiveLibrary[Testing &amp; Quality](/categories/testing)

k2gl/phpunit-fluent-assertions
==============================

Improves test code readability.

12.9.0(2d ago)29.4k↑278.6%12MITPHPPHP &gt;=8.1CI passing

Since Mar 8Pushed 4d ago1 watchersCompare

[ Source](https://github.com/k2gl/phpunit-fluent-assertions)[ Packagist](https://packagist.org/packages/k2gl/phpunit-fluent-assertions)[ Docs](https://github.com/k2gl/phpunit-fluent-assertions)[ RSS](/packages/k2gl-phpunit-fluent-assertions/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (2)Dependencies (7)Versions (34)Used By (12)

Fluent assertions for PHPUnit
=============================

[](#fluent-assertions-for-phpunit)

This library is inspired by [Vladimir Khorikov](https://enterprisecraftsmanship.com/), the author of [Unit Testing: Principles, Patterns and Practices](https://enterprisecraftsmanship.com/book-amazon) and makes checks in tests more readable.

[![CI](https://camo.githubusercontent.com/fcfa026f65603b0ce6b1e91025622a3b4155019d4c271f25069e822a90472e3e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6b32676c2f706870756e69742d666c75656e742d617373657274696f6e732f63692e796d6c3f6272616e63683d6d61696e266c6162656c3d4349266c6f676f3d676974687562)](https://github.com/k2gl/phpunit-fluent-assertions/actions/workflows/ci.yml)[![Latest Stable Version](https://camo.githubusercontent.com/b24d0cc04082479ec3f17317aeabf048e64b1fdf61347481b51ad5e018f8fc5a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6b32676c2f706870756e69742d666c75656e742d617373657274696f6e733f6c6f676f3d7061636b6167697374266c6f676f436f6c6f723d7768697465)](https://packagist.org/packages/k2gl/phpunit-fluent-assertions)[![Total Downloads](https://camo.githubusercontent.com/bf15a2a58e42695176bd3783d809645650dc3d2cfda0583b224c641027c4dc0c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6b32676c2f706870756e69742d666c75656e742d617373657274696f6e733f6c6f676f3d7061636b6167697374266c6f676f436f6c6f723d7768697465)](https://packagist.org/packages/k2gl/phpunit-fluent-assertions)[![PHPStan Level](https://camo.githubusercontent.com/01c58e66f2fafb70c17613ff2b1da3f549aade3a735b076da5cd9e5c04b945a5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d3261356561373f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://phpstan.org)[![License](https://camo.githubusercontent.com/6dd4a8fe751227baaeff79d24bac44a9bc43c33de89d792e2474ce1c7f4553c2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6b32676c2f706870756e69742d666c75656e742d617373657274696f6e733f636f6c6f723d79656c6c6f77677265656e)](https://packagist.org/packages/k2gl/phpunit-fluent-assertions)

Installation
------------

[](#installation)

You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/):

```
composer require --dev k2gl/phpunit-fluent-assertions

```

Usage
-----

[](#usage)

Write tests as usual, just use fluent assertions short aliases `check($x)->...;`, `expect($x)->...;` or `fact($x)->...;` instead of `self::assert...($x, $y)`.

```
// arrange
$user = UserFactory::createOne();

// act
$user->setPhone($e164PhoneNumber = faker()->e164PhoneNumber);

// traditional PHPUnit assertions
self::assertSame($e164PhoneNumber, $user->getPhone());

// fluent assertions
fact($user->getPhone())
    ->is($e164PhoneNumber)
    ->isString()
    ->startsWith('+7')
    // etc.
    ;
```

### Array assertions

[](#array-assertions)

```
fact([1, 2, 3])->count(3); // Passes
fact([1, 2])->count(3); // Fails

fact([1, 2])->notCount(3); // Passes
fact([1, 2, 3])->notCount(3); // Fails

fact(['a' => ['b' => 'c']])->arrayContainsAssociativeArray(['a' => ['b' => 'c']]); // Passes
fact(['a' => ['b' => 'd']])->arrayContainsAssociativeArray(['a' => ['b' => 'c']]); // Fails

fact(['a' => 1])->arrayHasKey('a'); // Passes
fact(['a' => 1])->arrayHasKey('b'); // Fails

fact(['a' => 1])->arrayNotHasKey('b'); // Passes
fact(['a' => 1])->arrayNotHasKey('a'); // Fails

fact([1, 2, 3])->contains(2); // Passes
fact([1, 2])->contains(3); // Fails

fact([1, 2])->doesNotContain(3); // Passes
fact([1, 2, 3])->doesNotContain(3); // Fails

fact([1, 2])->hasSize(2); // Passes
fact([1, 2, 3])->hasSize(2); // Fails

fact([])->isEmptyArray(); // Passes
fact([1, 2])->isEmptyArray(); // Fails

fact([1, 2])->isNotEmptyArray(); // Passes
fact([])->isNotEmptyArray(); // Fails

fact([2, 4, 6])->every(fn($v) => $v % 2 === 0); // Passes
fact([1, 2, 3])->every(fn($v) => $v > 5); // Fails

fact([1, 2, 3])->some(fn($v) => $v > 2); // Passes
fact([1, 2, 3])->some(fn($v) => $v > 10); // Fails

fact([1, 2, 3])->none(fn($v) => $v > 10); // Passes
fact([1, 2, 3])->none(fn($v) => $v > 2); // Fails
```

### Boolean assertions

[](#boolean-assertions)

```
fact(true)->true(); // Passes
fact(1)->true(); // Fails due to strict comparison

fact(false)->notTrue(); // Passes
fact(true)->notTrue(); // Fails

fact(false)->false(); // Passes
fact(0)->false(); // Fails due to strict comparison

fact(true)->notFalse(); // Passes
fact(false)->notFalse(); // Fails
```

### Comparison and equality assertions

[](#comparison-and-equality-assertions)

```
fact(42)->is(42); // Passes
fact(42)->is('42'); // Fails due to type difference

fact(42)->equals(42); // Passes
fact(42)->equals('42'); // Passes due to loose comparison

fact(42)->not(43); // Passes
fact(42)->not(42); // Fails
```

### Null assertions

[](#null-assertions)

```
fact(null)->null(); // Passes
fact('')->null(); // Fails

fact(42)->notNull(); // Passes
fact(null)->notNull(); // Fails
```

### Numeric assertions

[](#numeric-assertions)

```
fact(5)->isLowerThan(10); // Passes
fact(10)->isLowerThan(5); // Fails

fact(10)->isGreaterThan(5); // Passes
fact(5)->isGreaterThan(10); // Fails

fact(5)->isPositive(); // Passes
fact(-3)->isPositive(); // Fails

fact(-3)->isNegative(); // Passes
fact(5)->isNegative(); // Fails

fact(0)->isZero(); // Passes
fact(0.0)->isZero(); // Passes
fact(1)->isZero(); // Fails

fact(5)->isBetween(1, 10); // Passes
fact(15)->isBetween(1, 10); // Fails
```

### String assertions

[](#string-assertions)

```
fact('abc123')->matchesRegularExpression('/^[a-z]+\d+$/'); // Passes
fact('123abc')->matchesRegularExpression('/^[a-z]+\d+$/'); // Fails

fact('123abc')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Passes
fact('abc123')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Fails

fact('hello world')->containsString('world'); // Passes
fact('hello world')->containsString('foo'); // Fails

fact('hello world')->notContainsString('foo'); // Passes
fact('hello world')->notContainsString('world'); // Fails

fact('Hello World')->containsStringIgnoringCase('world'); // Passes
fact('Hello World')->containsStringIgnoringCase('foo'); // Fails

fact('Hello World')->notContainsStringIgnoringCase('foo'); // Passes
fact('Hello World')->notContainsStringIgnoringCase('world'); // Fails

fact('hello world')->startsWith('hello'); // Passes
fact('world hello')->startsWith('hello'); // Fails

fact('file.txt')->endsWith('.txt'); // Passes
fact('txt.file')->endsWith('.txt'); // Fails

fact('abc')->hasLength(3); // Passes
fact('abcd')->hasLength(3); // Fails

fact('')->isEmptyString(); // Passes
fact('hello')->isEmptyString(); // Fails

fact('hello')->isNotEmptyString(); // Passes
fact('')->isNotEmptyString(); // Fails

fact('{"key": "value"}')->isJson(); // Passes
fact('invalid json')->isJson(); // Fails

fact('{"a":1,"b":2}')->matchesJson('{"b":2,"a":1}'); // Passes (key order ignored)
fact('{"a":1}')->matchesJson('{"a":2}'); // Fails

fact('{"a":1}')->notMatchesJson('{"a":2}'); // Passes
fact('{"a":1,"b":2}')->notMatchesJson('{"b":2,"a":1}'); // Fails

fact('user@example.com')->isValidEmail(); // Passes
fact('invalid-email')->isValidEmail(); // Fails

fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID)
fact('invalid-ulid')->ulid(); // Fails
```

### Type Checking assertions

[](#type-checking-assertions)

```
fact(new stdClass())->instanceOf(stdClass::class); // Passes
fact(new stdClass())->instanceOf(Exception::class); // Fails

fact(new stdClass())->notInstanceOf(Exception::class); // Passes
fact(new stdClass())->notInstanceOf(stdClass::class); // Fails

fact(42)->isInt(); // Passes
fact('42')->isInt(); // Fails

fact('text')->isString(); // Passes
fact(42)->isString(); // Fails

fact((object)['name' => 'John'])->hasProperty('name'); // Passes
fact((object)['name' => 'John'])->hasProperty('age'); // Fails

fact(new stdClass())->hasMethod('__construct'); // Passes
fact(new stdClass())->hasMethod('nonExistentMethod'); // Fails

fact(3.14)->isFloat(); // Passes
fact(42)->isFloat(); // Fails

fact(true)->isBool(); // Passes
fact(1)->isBool(); // Fails

fact([1, 2])->isArray(); // Passes
fact('not array')->isArray(); // Fails

fact(fopen('php://memory', 'r'))->isResource(); // Passes
fact('string')->isResource(); // Fails

fact('strlen')->isCallable(); // Passes
fact(123)->isCallable(); // Fails

fact(3.14)->isFloat(); // Passes
fact(42)->isFloat(); // Fails

fact(true)->isBool(); // Passes
fact(1)->isBool(); // Fails
```

### Exception assertions

[](#exception-assertions)

The subject is a callable; `throws()` invokes it and asserts the expected exception is thrown. Pass a message substring to also assert on the exception message.

```
fact(fn () => throw new RuntimeException('boom'))->throws(RuntimeException::class); // Passes
fact(fn () => $service->run())->throws(DomainException::class, 'invalid'); // Passes if message contains "invalid"
fact(fn () => 42)->throws(RuntimeException::class); // Fails — nothing thrown
```

### Date/time assertions

[](#datetime-assertions)

The subject is a `DateTimeInterface`; a string expectation is parsed as a `DateTimeImmutable`.

```
fact(new DateTimeImmutable('2024-01-01'))->isBefore('2024-12-31'); // Passes
fact(new DateTimeImmutable('2024-12-31'))->isAfter('2024-01-01'); // Passes
fact(new DateTimeImmutable('2024-06-13 23:59'))->isSameDate('2024-06-13'); // Passes — same calendar date, time ignored
```

### Enum assertions

[](#enum-assertions)

Work on any native `UnitEnum` / `BackedEnum`.

```
fact($order->status)->isEnum(Status::Paid);   // identity: the subject is that case
fact(Status::Paid)->hasValue('paid');          // BackedEnum backing value
fact(Status::Paid)->hasName('Paid');           // case name
```

PHPStan support
---------------

[](#phpstan-support)

The package ships a PHPStan extension that narrows the asserted value, so the analyser keeps following your types after a fluent assertion:

```
/** @var array{something: string}|null $context */
$context = $user->getContext();

echo $context['something']; // PHPStan error: Cannot access offset 'something' on array{something: string}|null

fact($context)->notNull();

echo $context['something']; // OK: $context is narrowed to array{something: string}
```

More examples — every supported assertion narrows the subject in place:

```
// instanceOf(): a union is narrowed to the concrete class
/** @var DateTimeInterface|null $date */
fact($date)->instanceOf(DateTimeImmutable::class);
// → $date is DateTimeImmutable

// notInstanceOf(): the class is subtracted from the union
/** @var DateTime|DateTimeImmutable $createdAt */
fact($createdAt)->notInstanceOf(DateTimeImmutable::class);
// → $createdAt is DateTime

// true() / false(): a bool is narrowed to the literal
/** @var bool $flag */
fact($flag)->true();      // → $flag is true
/** @var bool $enabled */
check($enabled)->false(); // → $enabled is false

// is(): narrowed to the exact expected value (assertSame semantics)
/** @var mixed $status */
fact($status)->is('active');
// → $status is 'active'

// null(): narrowed to null
/** @var string|null $token */
fact($token)->null();
// → $token is null

// type checks: narrowed to the asserted PHP type
/** @var mixed $value */
fact($value)->isString();
// → $value is string  (likewise isInt/isFloat/isBool/isArray/isCallable/isResource)

// chains accumulate every supported step
/** @var string|null $name */
fact($name)->notNull()->is('admin');
// → $name is 'admin'
```

The chain can be opened with any of `check()`, `expect()`, `fact()` or `FluentAssertions::for()`, and the subject may be a variable or a property (e.g. `fact($user->getContext())->notNull()`).

If you use [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer)it is picked up automatically. Otherwise include it manually in your `phpstan.neon`:

```
includes:
    - vendor/k2gl/phpunit-fluent-assertions/extension.neon
```

Narrowing is applied for `notNull()`, `null()`, `true()`, `notTrue()`, `false()`, `notFalse()`, `instanceOf()`, `notInstanceOf()`, `is()`, the type checks `isString()`, `isInt()`, `isFloat()`, `isBool()`, `isArray()`, `isCallable()` and `isResource()`, and the JSON assertions `isJson()`, `matchesJson()` and `notMatchesJson()` (subject narrowed to `string`). Loose or negated assertions such as `equals()` (loose `==`) and `not()` would not narrow soundly, so they are intentionally left out and leave the type unchanged.

Pull requests are always welcome
--------------------------------

[](#pull-requests-are-always-welcome)

[Collaborate with pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance99

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity66

Established project with proven stability

 Bus Factor1

Top contributor holds 98.1% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~39 days

Recently: every ~8 days

Total

32

Last Release

2d ago

Major Versions

1.13.1 → 11.0.02024-04-21

11.0.2 → 12.0.02025-05-01

### Community

Maintainers

![](https://www.gravatar.com/avatar/6bc4aa529c7f13ea593297497f6eae20d5c07f476baa0a551960d7e6ff1e5413?d=identicon)[k2gl](/maintainers/k2gl)

---

Top Contributors

[![k2gl](https://avatars.githubusercontent.com/u/2846079?v=4)](https://github.com/k2gl "k2gl (105 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")

---

Tags

testingphpunitxunit

###  Code Quality

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/k2gl-phpunit-fluent-assertions/health.svg)

```
[![Health](https://phpackages.com/badges/k2gl-phpunit-fluent-assertions/health.svg)](https://phpackages.com/packages/k2gl-phpunit-fluent-assertions)
```

###  Alternatives

[phpunit/phpunit

The PHP Unit Testing framework.

20.0k955.1M155.1k](/packages/phpunit-phpunit)[brianium/paratest

Parallel testing for PHP

2.5k136.1M986](/packages/brianium-paratest)[phpunit/phpunit-selenium

Selenium Server integration for PHPUnit

61011.1M165](/packages/phpunit-phpunit-selenium)[spatie/phpunit-snapshot-assertions

Snapshot testing with PHPUnit

69619.8M640](/packages/spatie-phpunit-snapshot-assertions)[allure-framework/allure-phpunit

Allure PHPUnit integration

6913.4M46](/packages/allure-framework-allure-phpunit)[facile-it/paraunit

paraunit

145845.1k17](/packages/facile-it-paraunit)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
