zenstruck / assert
独立、轻量级、不依赖框架的测试断言库。
Requires
- php: >=8.0
- symfony/polyfill-php81: ^1.23
- symfony/var-exporter: ^5.4|^6.0|^7.0
Requires (Dev)
- phpstan/phpstan: ^1.4
- phpunit/phpunit: ^9.5
- symfony/phpunit-bridge: ^6.3
README
这个库允许无依赖的测试断言。当使用基于PHPUnit的测试库(包括PHPUnit本身、Pest、Codeception)时,失败的断言会自动转换为PHPUnit测试失败,成功的断言会增加PHPUnit的成功断言计数。
与其他流行的断言库(如webmozart/assert 和 beberlei/assert)相比,这个库纯粹用于测试断言,而不仅仅是它们提供的类型安全断言。
除了期望API(特别是抛出期望,它提供了一个制作异常断言的便捷API)外,这个库对希望提供测试断言但不想直接依赖特定测试库的第三方库非常有用。
安装
$ composer require zenstruck/assert
Zenstruck\Assert
这是进行断言的主要入口点。当调用这个类上的方法时,在失败的情况下抛出Zenstruck\Assert\AssertionFailed
异常。如果没有抛出这个异常,则认为它是成功的。
当使用基于PHPUnit的框架时,失败的断言会自动转换为PHPUnit测试失败,成功的断言会增加PHPUnit的成功断言计数。
真/假断言
use Zenstruck\Assert; // passes Assert::true(true === true, 'The condition was not true.'); // fails Assert::true(true === false, 'The condition was not true.'); // passes Assert::false(true === false, 'The condition was not false.'); // fails Assert::false(true === true, 'The condition was not false.');
通用失败/通过
use Zenstruck\Assert; // trigger a "fail" Assert::fail('This is a failure.'); // trigger a "pass" Assert::pass();
尝试
尝试运行回调并返回结果。如果在运行过程中抛出异常,则触发失败。如果运行成功,则触发通过。
use Zenstruck\Assert; $ret = Assert::try(fn() => 'value'); // $ret === 'value' Assert::try(fn() => throw new \RuntimeException('exception message')); // "fails" with message "exception message" // customize the failure message Assert::try( fn() => throw new \RuntimeException('exception message'), 'Tried to run the code but {exception} with message "{message}" was thrown.' ); // "fails" with message 'Tried to run the code but RuntimeException with message "exception message" was thrown.'
运行断言
Assert::run()
执行一个callable
。成功的执行被视为通过,如果抛出Zenstruck\Assert\AssertionFailed
异常,则视为失败。
use Zenstruck\Assert; use Zenstruck\Assert\AssertionFailed; // failure Assert::run(function(): void { if (true) { AssertionFailed::throw('This failed.'); } }); // pass Assert::run(function(): void { if (false) { AssertionFailed::throw('This failed.'); } });
期望API
虽然可以使用上述断言创建任何断言,但提供了一个简单、流畅、可读的期望API。这个API深受Pest PHP的启发。
use Zenstruck\Assert; // empty Assert::that([])->isEmpty(); // pass Assert::that(['foo'])->isEmpty(); // fail Assert::that(null)->isNotEmpty(); // fail Assert::that('value')->isNotEmpty(); // pass // null Assert::that(null)->isNull(); // pass Assert::that('foo')->isNull(); // fail Assert::that(null)->isNotNull(); // fail Assert::that('value')->isNotNull(); // pass // count Assert::that([1, 2])->hasCount(2); // pass Assert::that(new \ArrayIterator([1, 2, 3]))->hasCount(2); // fail Assert::that(new \ArrayIterator([1, 2]))->doesNotHaveCount(5); // pass Assert::that($countableObjectWithCountOf5)->doesNotHaveCount(5); // fail // contains Assert::that('foobar')->contains('foo'); // pass Assert::that(['foo', 'bar'])->contains('foo'); // pass Assert::that('foobar')->contains('baz'); // fail Assert::that(['foo', 'bar'])->contains(6); // fail Assert::that('foobar')->doesNotContain('baz'); // pass Assert::that(new \ArrayIterator(['bar']))->doesNotContain('foo'); // pass Assert::that('foobar')->doesNotContain('bar'); // fail Assert::that(['foo', 'bar'])->doesNotContain('bar'); // fail // array subsets Assert::that(['foo' => 'bar'])->isSubsetOf(['foo' => 'bar', 'bar' => 'foo']); // pass Assert::that(['foo' => 'bar'])->isSubsetOf(['bar' => 'foo']); // fail Assert::that(['foo' => 'bar', 'bar' => 'foo'])->hasSubset(['foo' => 'bar']); // pass Assert::that(['foo' => 'bar'])->hasSubset(['bar' => 'foo']); // fail // array subset assertions can also be performed on non-associated arrays (lists/sets). // Keep in mind that order does not matter. Assert::that([ 'users' => [ ['name' => 'user3', 'age' => 20], ['name' => 'user1'], ] ])->isSubsetOf([ 'users' => [ ['name' => 'user1', 'age' => 25], ['name' => 'user2', 'age' => 23], ['name' => 'user3', 'age' => 20], ] ]); // pass // also works with json strings that decode to arrays Assert::that('[3, 1]')->isSubsetOf('[1, 2, 3]'); // pass // equals (== comparison) Assert::that('foo')->equals('foo'); // pass Assert::that('6')->equals(6); // pass Assert::that('foo')->equals('bar'); // fail Assert::that(6)->equals(7); // fail Assert::that('foo')->isNotEqualTo('bar'); // pass Assert::that(6)->isNotEqualTo('6'); // fail // is (=== comparison) Assert::that('foo')->is('foo'); // pass Assert::that(6)->is(6); // pass Assert::that('foo')->is('bar'); // fail Assert::that(6)->is('6'); // fail Assert::that('foo')->isNot('foo'); // fail Assert::that(6)->isNot(6); // fail Assert::that('foo')->isNot('bar'); // pass Assert::that(6)->isNot('6'); // pass // boolean (===) Assert::that(true)->isTrue(); // pass Assert::that(false)->isTrue(); // fail Assert::that(true)->isFalse(); // fail Assert::that(false)->isFalse(); // pass // boolean (==) Assert::that(1)->isTruthy(); // pass Assert::that(new \stdClass())->isTruthy(); // pass Assert::that('text')->isTruthy(); // pass Assert::that(null)->isTruthy(); // fail Assert::that(0)->isFalsy(); // pass Assert::that(null)->isFalsy(); // pass Assert::that('')->isFalsy(); // pass Assert::that(1)->isFalsy(); // fail // instanceof Assert::that($object)->isInstanceOf(Some::class); Assert::that($object)->isNotInstanceOf(Some::class); // greater than Assert::that(2)->isGreaterThan(1); // pass Assert::that(2)->isGreaterThan(1); // fail Assert::that(2)->isGreaterThan(2); // fail // greater than or equal to Assert::that(2)->isGreaterThanOrEqualTo(1); // pass Assert::that(2)->isGreaterThanOrEqualTo(1); // fail Assert::that(2)->isGreaterThanOrEqualTo(2); // pass // less than Assert::that(3)->isLessThan(4); // pass Assert::that(3)->isLessThan(2); // fail Assert::that(3)->isLessThan(3); // fail // less than or equal to Assert::that(3)->isLessThanOrEqualTo(4); // pass Assert::that(3)->isLessThanOrEqualTo(2); // fail Assert::that(3)->isLessThanOrEqualTo(3); // pass
类型期望
use Zenstruck\Assert; use Zenstruck\Assert\Type; Assert::that($something)->is(Type::bool()); Assert::that($something)->is(Type::int()); Assert::that($something)->is(Type::float()); Assert::that($something)->is(Type::numeric()); Assert::that($something)->is(Type::string()); Assert::that($something)->is(Type::callable()); Assert::that($something)->is(Type::iterable()); Assert::that($something)->is(Type::countable()); Assert::that($something)->is(Type::object()); Assert::that($something)->is(Type::resource()); Assert::that($something)->is(Type::array()); Assert::that($something)->is(Type::arrayList()); // [1, 2, 3] passes but ['foo' => 'bar'] does not Assert::that($something)->is(Type::arrayAssoc()); // ['foo' => 'bar'] passes but [1, 2, 3] does not Assert::that($something)->is(Type::arrayEmpty()); // [] passes but [1, 2, 3] does not Assert::that($something)->is(Type::json()); // valid json string // "Not's" Assert::that($something)->isNot(Type::bool()); Assert::that($something)->isNot(Type::int()); Assert::that($something)->isNot(Type::float()); Assert::that($something)->isNot(Type::numeric()); Assert::that($something)->isNot(Type::string()); Assert::that($something)->isNot(Type::callable()); Assert::that($something)->isNot(Type::iterable()); Assert::that($something)->isNot(Type::countable()); Assert::that($something)->isNot(Type::object()); Assert::that($something)->isNot(Type::resource()); Assert::that($something)->isNot(Type::array()); Assert::that($something)->isNot(Type::arrayList()); Assert::that($something)->isNot(Type::arrayAssoc()); Assert::that($something)->isNot(Type::arrayEmpty()); Assert::that($something)->isNot(Type::json());
抛出期望
这个期望提供了一个制作异常的便捷API。它是PHPUnit的expectException()
的替代品,具有以下限制:
- 每个测试只能断言抛出1个异常。
- 不能对异常本身进行断言(除了消息之外)。
- 不能进行异常后的断言(考虑副作用)。
use Zenstruck\Assert; // the following can all be used within a single PHPUnit test // fails if exception not thrown // fails if exception is thrown but not instance of \RuntimeException Assert::that(fn() => $code->thatThrowsException())->throws(\RuntimeException::class); // fails if exception not thrown // fails if exception is thrown but not instance of \RuntimeException // fails if exception is thrown but exception message doesn't contain "some message" Assert::that(fn() => $code->thatThrowsException())->throws(\RuntimeException::class, 'some message'); // a callable can be used for the expected exception. The first parameter's type // hint is used as the expected exception and the callable is executed with the // caught exception // // fails if exception not thrown // fails if exception is thrown but not instance of CustomException Assert::that(fn() => $code->thatThrowsException())->throws( function(CustomException $e) use ($database) { // make assertions on the exception Assert::that($e->getMessage())->contains('some message'); Assert::that($e->getSomeValue())->is('value'); // make side effect assertions Assert::true($database->userTableEmpty(), 'The user table is not empty'); // If using within the context of a PHPUnit test, you can use standard PHPUnit assertions $this->assertStringContainsString('some message', $e->getMessage()); $this->assertSame('value', $e->getSomeValue()); $this->assertTrue($database->userTableEmpty()); } );
流畅期望
use Zenstruck\Assert; // chain expectations on the same "value" Assert::that(['foo', 'bar']) ->hasCount(2) ->contains('foo') ->contains('bar') ->doesNotContain('baz') ; // start an additional expectation without breaking Assert::that(['foo', 'bar']) ->hasCount(2) ->contains('foo') ->and('foobar') // start a new expectation with "foobar" as the new expectation value ->contains('bar') ;
AssertionFailed
异常
在触发失败的断言时,为用户提供有用的失败信息非常重要。AssertionFailed
异常有一些特性可以帮助做到这一点。
use Zenstruck\Assert\AssertionFailed; // The `throw()` named constructor creates the exception and immediately throws it AssertionFailed::throw('Some message'); // a second "context" parameter can be used as sprintf values for the message AssertionFailed::throw('Expected "%s" but got "%s"', ['value 1', 'value 2']); // Expected "value 1" but got "value 2" // when an associated array passed as the context parameter, the message is constructed // with a simple template system AssertionFailed::throw('Expected "{expected}" but got "{actual}"', [ // Expected "value 1" but got "value 2" 'expected' => 'value 1', 'actual' => 'value 2', ]);
NOTES
- 当消息与上下文构造时,非标量值会通过
get_debug_type()
运行,超过100个字符的字符串会被截断。完整的上下文可以通过AssertionFailed::context()
获取。 - 当与PHPUnit一起使用时,如果处于详细模式(
--verbose|-v
),则会将完整的上下文与失败消息一起导出。
断言对象
由于Zenstruck\Assert::run()
接受任何callable
,因此可以将复杂的断言封装成可调用的对象。
use Zenstruck\Assert; use Zenstruck\Assert\AssertionFailed; class StringContains { public function __construct(private string $haystack, private string $needle) {} public function __invoke(): void { if (!str_contains($this->haystack, $this->needle)) { AssertionFailed::throw( 'Expected string "{haystack}" to contain "{needle}" but it did not.', get_object_vars($this) ]); } } } // use the above assertion: // passes Assert::run(new StringContains('quick brown fox', 'fox')); // fails Assert::run(new StringContains('quick brown fox', 'dog'));
可否定断言对象
Zenstruck\Assert
拥有一个 not()
方法,可以与 可否定 断言对象 一起使用。这有助于创建可以轻松否定的自定义断言。让我们将上面的示例转换为 可否定断言对象
use Zenstruck\Assert; use Zenstruck\Assert\AssertionFailed; use Zenstruck\Assert\Assertion\Negatable; class StringContains implements Negatable { public function __construct(private string $haystack, private string $needle) {} public function __invoke(): void { if (!str_contains($this->haystack, $this->needle)) { AssertionFailed::throw( 'Expected string "{haystack}" to contain "{needle}" but it did not.', get_object_vars($this) ]); } } public function notFailure(): AssertionFailed { return new AssertionFailed( 'Expected string "{haystack}" to not contain "{needle}" but it did.', get_object_vars($this) ); } } // use the above assertion: // fails Assert::not(new StringContains('quick brown fox', 'fox')); // passes Assert::not(new StringContains('quick brown fox', 'dog'));