zenstruck/assert

独立、轻量级、不依赖框架的测试断言库。

资助包维护!
kbond

v1.5.0 2023-12-02 09:08 UTC

This package is auto-updated.

Last update: 2024-09-19 14:11:41 UTC


README

CI Status Code Coverage

这个库允许无依赖的测试断言。当使用基于PHPUnit的测试库(包括PHPUnit本身、Pest、Codeception)时,失败的断言会自动转换为PHPUnit测试失败,成功的断言会增加PHPUnit的成功断言计数。

与其他流行的断言库(如webmozart/assertbeberlei/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. 每个测试只能断言抛出1个异常。
  2. 不能对异常本身进行断言(除了消息之外)。
  3. 不能进行异常后的断言(考虑副作用)。
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

  1. 当消息与上下文构造时,非标量值会通过get_debug_type()运行,超过100个字符的字符串会被截断。完整的上下文可以通过AssertionFailed::context()获取。
  2. 当与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'));