bovigo/assert

为单元测试提供断言。

v8.0.1 2024-01-27 20:34 UTC

README

为单元测试提供断言。

包状态

Build Status Coverage Status

Latest Stable Version Latest Unstable Version

安装

bovigo/assert 以 Composer 包的形式分发。要将它安装为您的包的开发依赖项,请使用以下命令

composer require --dev "bovigo/assert": "^8.0"

要将它安装为您的包的运行时依赖项,请使用以下命令

composer require "bovigo/assert=^7.0"

要求

bovigo/assert 8.x 至少需要 PHP 8.2。

为什么?

最初的想法是探索如何使用断言在单元测试中采用更函数式的方法,并看看这是否会使得测试代码的阅读变得更好。我个人认为结果足够有说服力,因此我想在自己的代码中使用它,所以我创建了一个包。

用法

所有断言都使用函数以相同的方式编写

assertThat(303, equals(303));
assertThat($someArray, isOfSize(3), 'array always must have size 3');

第一个参数是要测试的值,第二个是要用来测试值的谓词。此外,还可以提供一个可选的描述以增强断言失败时的清晰度。

如果谓词失败,则会抛出 AssertionFailure,并提供有关测试失败原因的有用信息。如果使用 PHPUnit,则 AssertionFailure\PHPUnit\Framework\AssertionFailedError 的实例,因此它可以很好地集成到 PHPUnit 中,产生与 PHPUnit 约束类似的测试输出。以下是测试失败时输出的示例

1) bovigo\assert\predicate\RegexTest::stringRepresentationContainsRegex
Failed asserting that 'matches regular expression "/^([a-z]{3})$/"' is equal to <string:matches regular expession "/^([a-z]{3})$/">.
--- Expected
+++ Actual
@@ @@
-'matches regular expession "/^([a-z]{3})$/"'
+'matches regular expression "/^([a-z]{3})$/"'

bovigo-assert/src/test/php/predicate/RegexTest.php:99

为了简洁起见,下面假设使用的函数通过以下方式导入到当前命名空间

use function bovigo\assert\assertThat;
use function bovigo\assert\predicate\isOfSize;
use function bovigo\assert\predicate\equals;
// ... and so on

谓词列表

以下是默认包含在 bovigo/assert 中的谓词列表。

isNull()

测试值是否为 null

assertThat($value, isNull());

别名: bovigo\assert\assertNull($value, $description = null)

isNotNull()

测试值是否不为 null

assertThat($value, isNotNull());

别名: bovigo\assert\assertNotNull($value, $description = null)

isEmpty()

测试值是否为空。空定义为以下内容

  • 如果值是 \Countable 的实例,则当它的计数为 0 时为空。
  • 对于所有其他值,PHP 的 empty() 规则适用。
assertThat($value, isEmpty());

别名

  • bovigo\assert\assertEmpty($value, $description = null)
  • bovigo\assert\assertEmptyString($value, $description = null)
  • bovigo\assert\assertEmptyArray($value, $description = null)

isNotEmpty()

测试值是否不为空。有关空白的定义,请参阅 isEmpty()

assertThat($value, isNotEmpty());

别名: bovigo\assert\assertNotEmpty($value, $description = null)

isTrue()

测试值是否为真。值必须是布尔值真,不进行值转换。

assertThat($value, isTrue());

别名: bovigo\assert\assertTrue($value, $description = null)

isFalse()

测试值是否为假。值必须是布尔值假,不进行值转换。

assertThat($value, isFalse());

别名: bovigo\assert\assertFalse($value, $description = null)

equals($expected)

测试值是否等于预期值。如果需要测试浮点数的相等性,则可以使用可选参数 $delta,它允许在一定范围内将两个浮点数视为相等。

assertThat($value, equals('Roland TB 303'));

如果需要 delta,例如对于浮点数,可以设置所需的 delta

assertThat($value, equals(5)->withDelta(0.1));

isNotEqualTo($unexpected)

测试一个值是否不等于意外值。当需要测试浮点值的相等性时,可以使用可选参数 $delta,它允许在两个浮点值被认为是相等的一定范围内。

assertThat($value, isNotEqualTo('Roland TB 303'));

如果需要 delta,例如对于浮点数,可以设置所需的 delta

assertThat($value, isNotEqualTo(5)->withDelta(0.1));

isInstanceOf($expectedType)

测试一个值是否是期望类型的实例。

assertThat($value, isInstanceOf(\stdClass::class));

isNotInstanceOf($unexpectedType)

测试一个值不是意外类型的实例。

assertThat($value, isNotInstanceOf(\stdClass::class));

isSameAs($expected)

测试一个值与期望值完全相同。两个值都使用 === 进行比较,适用相应的规则。

assertThat($value, isSameAs($anotherValue));

isNotSameAs($unexpected)

测试一个值与意外值不完全相同。两个值都使用 === 进行比较,适用相应的规则。

assertThat($value, isNotSameAs($anotherValue));

isOfSize($expectedSize)

测试一个值具有期望的大小。大小规则如下:

  • 对于字符串,使用其字节数长度。
  • 对于数组以及 \Countable 的实例,使用 count() 的值。
  • 对于 \Traversable 的实例,使用 iterator_count() 的值。为了避免移动可遍历对象的指针,对可遍历对象的副本应用 iterator_count()
  • 所有其他值类型都将被拒绝。
assertThat($value, isOfSize(3));

isNotOfSize($unexpectedSize)

测试一个值不具有意外的大小。规则与 isOfSize($expectedSize) 相同。

assertThat($value, isNotOfSize(3));

isOfType($expectedType)

测试一个值是期望的内部 PHP 类型。

assertThat($value, isOfType('resource'));

别名

从 5.0 版本开始,提供了一些别名函数以防止在该函数使用中出错。

  • bovigo\assert\predicate\isArray()
  • bovigo\assert\predicate\isBool()
  • bovigo\assert\predicate\isFloat()
  • bovigo\assert\predicate\isInt()
  • bovigo\assert\predicate\isNumeric()
  • bovigo\assert\predicate\isObject()
  • bovigo\assert\predicate\isResource()
  • bovigo\assert\predicate\isString()
  • bovigo\assert\predicate\isScalar()
  • bovigo\assert\predicate\isCallable()
  • bovigo\assert\predicate\isIterable()

isNotOfType($unexpectedType)

测试一个值不是意外的内部 PHP 类型。

assertThat($value, isNotOfType('resource'));

别名

从 5.0 版本开始,提供了一些别名函数以防止在该函数使用中出错。请注意,其中一些函数是特定的,以确保使用它们编写的代码构成语法上有效的句子。

  • bovigo\assert\predicate\isNotAnArray()
  • bovigo\assert\predicate\isNotBool()
  • bovigo\assert\predicate\isNotFloat()
  • bovigo\assert\predicate\isNotInt()
  • bovigo\assert\predicate\isNotNumeric()
  • bovigo\assert\predicate\isNotAnObject()
  • bovigo\assert\predicate\isNotAResource()
  • bovigo\assert\predicate\isNotAString()
  • bovigo\assert\predicate\isNotScalar()
  • bovigo\assert\predicate\isNotCallable()
  • bovigo\assert\predicate\isNotIterable()

isGreaterThan($expected)

测试一个值是否大于期望值。

assertThat($value, isGreaterThan(3));

isGreaterThanOrEqualTo($expected)

测试一个值是否大于或等于期望值。

assertThat($value, isGreaterThanOrEqualTo(3));

isLessThan($expected)

测试一个值是否小于期望值。

assertThat($value, isLessThan(3));

isLessThanOrEqualTo($expected)

测试一个值是否小于或等于期望值。

assertThat($value, isLessThanOrEqualTo(3));

contains($needle)

测试 $needle 是否包含在值中。以下规则适用:

  • null 包含在 null 中。
  • 字符串可以包含在另一个字符串中。比较是区分大小写的。
  • $needle 可以是数组或 \Traversable 的值。值和 $needle 使用 === 进行比较。
  • 对于所有其他情况,值将被拒绝。
assertThat($value, contains('Roland TB 303'));

有时需要区分数组、可遍历和字符串。如果需要强制特定类型,建议组合谓词。

assertThat($value, isArray()->and(contains('Roland TB 303')));
assertThat($value, isString()->and(contains('Roland TB 303')));
assertThat($value, isInstanceOf(\Iterator::class)->and(contains('Roland TB 303')));

doesNotContain($needle)

测试 $needle 不包含在值中。适用于 contains($needle) 的规则。

assertThat($value, doesNotContain('Roland TB 303'));

hasKey($key)

测试数组或 \ArrayAccess 的实例是否具有给定名称的键。键必须是 integerstring 类型。既不是数组也不是 \ArrayAccess 实例的值将被拒绝。

assertThat($value, hasKey('roland'));

doesNotHaveKey($key)

测试数组或 \ArrayAccess 实例是否没有给定名称的键。该键必须是 integerstring 类型。既不是数组也不是 \ArrayAccess 实例的值将被拒绝。

assertThat($value, doesNotHaveKey('roland'));

containsSubset($other)

自 6.2.0 版本起可用。

测试 $other 是否包含该值。

assertThat($value, containsSubset(['TB-303', 'TR-808']));

matches($pattern)

测试字符串是否与正则表达式的给定模式匹配。如果值不是字符串,它将被拒绝。如果模式在值中至少产生一个匹配,则测试成功。

assertThat($value, matches('/^([a-z]{3})$/'));

doesNotMatch($pattern)

测试字符串是否与正则表达式的给定模式不匹配。如果值不是字符串,它将被拒绝。如果模式在值中没有匹配,则测试成功。

assertThat($value, doesNotMatch('/^([a-z]{3})$/'));

matchesFormat($format)

自 3.2.0 版本起可用。

测试字符串是否与给定的 PHP 格式表达式匹配。如果值不是字符串,它将被拒绝。如果格式在值中至少产生一个匹配,则测试成功。格式字符串可以包含以下占位符

  • %e:代表目录分隔符,例如 Linux 上的 /。
  • %s:一个或多个任何字符(字符或空白)除了换行符字符。
  • %S:零个或多个任何字符(字符或空白)除了换行符字符。
  • %a:一个或多个任何字符(字符或空白),包括换行符字符。
  • %A:零个或多个任何字符(字符或空白),包括换行符字符。
  • %w:零个或多个空白字符。
  • %i:有符号整数,例如 +3142, -3142。
  • %d:无符号整数,例如 123456。
  • %x:一个或多个十六进制字符。即 0-9, a-f, A-F 范围内的字符。
  • %f:浮点数,例如:3.142, -3.142, 3.142E-10, 3.142e+10。
  • %c:任何类型的单个字符。
assertThat($value, matchesFormat('%w'));

doesNotMatchFormat($format)

自 3.2.0 版本起可用。

测试字符串是否与给定的 PHP 格式表达式不匹配。如果值不是字符串,它将被拒绝。如果模式在值中没有匹配,则测试成功。参见上面的格式列表。

assertThat($value, doesNotMatchFormat('%w'));

isExistingFile($basePath = null)

测试值是否表示一个现有文件。如果没有提供 $basepath,值必须是绝对路径或当前工作目录的相对路径。当提供 $basepath 时,值必须是此基本路径的相对路径。

assertThat($value, isExistingFile());
assertThat($value, isExistingFile('/path/to/files'));

isNonExistingFile($basePath = null)

测试值是否表示一个不存在的文件。如果没有提供 $basepath,值必须是绝对路径或当前工作目录的相对路径。当提供 $basepath 时,值必须是此基本路径的相对路径。

assertThat($value, isNonExistingFile());
assertThat($value, isNonExistingFile('/path/to/files'));

isExistingDirectory($basePath = null)

测试值是否表示一个现有目录。如果没有提供 $basepath,值必须是绝对路径或当前工作目录的相对路径。当提供 $basepath 时,值必须是此基本路径的相对路径。

assertThat($value, isExistingDirectory());
assertThat($value, isExistingDirectory('/path/to/directories'));

isNonExistingDirectory($basePath = null)

测试值是否表示一个不存在的目录。如果没有提供 $basepath,值必须是绝对路径或当前工作目录的相对路径。当提供 $basepath 时,值必须是此基本路径的相对路径。

assertThat($value, isNonExistingDirectory());
assertThat($value, isNonExistingDirectory('/path/to/directories'));

startsWith($prefix)

自 1.1.0 版本起可用。

测试值必须是一个字符串,并且以给定的前缀开头。

assertThat($value, startsWith('foo'));

doesNotStartWith($prefix)

自 1.1.0 版本起可用。

测试值必须是一个字符串,并且不以给定的前缀开头。

assertThat($value, startsWith('foo'));

endsWith($suffix)

自 1.1.0 版本起可用。

测试值必须是一个字符串,并且以给定的后缀结尾。

assertThat($value, endsWith('foo'));

doesNotEndWith($suffix)

自 1.1.0 版本起可用。

测试必须为字符串的值不以给定后缀结尾。

assertThat($value, doesNotEndWith('foo'));

each($predicate)

自 1.1.0 版本起可用。

对数组或可遍历的每个值应用一个谓词。

assertThat($value, each(isInstanceOf($expectedType));

请注意,空数组或可遍历的结果将导致测试成功。如果它不能为空,请使用 isNotEmpty()->and(each($predicate))

assertThat($value, isNotEmpty()->and(each(isInstanceOf($expectedType))));

它也可以与任何可调用函数一起使用。

assertThat($value, each('is_nan'));
assertThat($value, each(function($value) { return substr($value, 4, 3) === 'foo'; }));

eachKey($predicate)

自1.3.0版本起可用。

对数组或可遍历的每个键应用一个谓词。

assertThat($value, eachKey(isOfType('int'));

请注意,空数组或可遍历的结果将导致测试成功。如果它不能为空,请使用 isNotEmpty()->and(eachKey($predicate))

assertThat($value, isNotEmpty()->and(eachKey(isOfType('int'))));

它也可以与任何可调用函数一起使用。

assertThat($value, eachKey('is_int'));
assertThat($value, eachKey(function($value) { return substr($value, 4, 3) === 'foo'; }));

not($predicate)

反转谓词的意义。

assertThat($value, not(isTrue()));

它也可以与任何可调用函数一起使用。

assertThat($value, not('is_nan'));
assertThat($value, not(function($value) { return substr($value, 4, 3) === 'foo'; }));

组合谓词

每个谓词提供两种方法将此谓词与另一个谓词组合成新的谓词。

and($predicate)

创建一个谓词,其中组合谓词必须都是 true 才能使组合谓词为 true。如果其中一个谓词失败,组合谓词也将失败。

assertThat($value, isNotEmpty()->and(eachKey(isOfType('int'))));

它也可以与任何可调用函数一起使用。

assertThat($value, isNotEmpty()->and('is_string'));

or($predicate)

创建一个谓词,其中一个组合谓词必须为 true。只有当所有谓词都失败时,组合谓词才会失败。

assertThat($value, equals(5)->or(isLessThan(5)));

它也可以与任何可调用函数一起使用。

assertThat($value, isNull()->or('is_finite'));

用户定义的谓词

要在断言中使用谓词,有两种可能性

使用可调用函数

您可以将任何 callable 传递给 assertThat() 函数

assertThat($value, 'is_nan');

这将创建一个使用 PHP 内置 is_nan() 函数测试值的谓词。

可调用函数应接受单个值(显然是要测试的值)并必须在成功时返回 true,在失败时返回 false。也可以抛出任何异常。

以下是一个使用闭包的示例

assertThat(
        $value,
        function($value)
        {
            if (!is_string($value)) {
                throw new \InvalidArgumentException(
                        'Given value is not a string.'
                );
            }

            return substr($value, 4, 3) === 'foo';
        }
);

扩展 bovigo\assert\predicate\Predicate

另一种可能性是扩展 bovigo\assert\predicate\Predicate 类。您需要实现以下方法之一

public function test($value)

此方法接收要测试的值并应在成功时返回 true,在失败时返回 false。也可以抛出任何异常。

public function __toString()

此方法必须返回谓词的正确描述,该描述适合在断言失败时显示的句子。这些句子如下组成

断言失败:[值的描述] [谓词的描述]。

此外,谓词可以通过覆盖 describeValue(Exporter $exporter, $value) 方法来影响 [值的描述]

立即失败

自1.2.0版本起可用。

如果在某些点上测试需要失败,而断言不足以做到这一点,可以使用 bovigo\assert\fail($description) 触发立即断言失败。

try {
    somethingThatThrowsFooException();
    fail('Expected ' . FooException::class . ', gone none');
} catch (FooException $fo) {
    // some assertions on FooException
}

phpstan 和早期终止函数调用

自5.1.0版本起可用

如果您使用 phpstanbovigo/assert 提供了一个配置文件,您可以将其包含在您的 phpstan 配置中,以便识别带有 fail() 的早期终止函数调用。

includes:
  - vendor/bovigo/assert/src/main/resources/phpstan/bovigo-assert.neon

期望

自1.6.0版本起可用

期望可以用来检查特定的代码片段是否抛出异常或触发错误。它还可以用来检查在特定的代码片段运行后,断言仍然为真,尽管相关的代码成功或失败。

对异常的期望

注意:自2.1.0版本起,也可以使用期望与 \Error 一起使用。

检查一个代码片段,例如函数或方法,是否抛出异常

expect(function() {
    // some piece of code which is expected to throw SomeException
})->throws(SomeException::class);

您还可以期望抛出任何异常,而不仅仅是特定的异常,通过省略异常的类名

expect(function() {
    // some piece of code which is expected to throw any exception
})->throws();

自2.1.0版本起,可以验证是否正好抛出了给定的异常

$exception = new \Exception('failure');
expect(function() use ($exception) {
    throw $exception;
})->throws($exception);

这将执行一个使用 isSameAs($exception) 的断言来验证抛出的异常。

此外,还可以对抛出的异常进行额外检查

expect(function() {
    // some piece of code which is expected to throw SomeException
})
->throws(SomeException::class)
->withMessage('some failure occured');

以下是对异常的检查方法

  • withMessage(string $expectedMessage) 对异常消息执行 equals() 断言。
  • message($predicate) 使用给定的谓词对异常消息执行断言。
  • withCode(int $expectedCode) 对异常代码执行 equals() 断言。
  • with($predicate) 使用给定的谓词对整个异常执行断言。谓词将接收异常作为参数,并可以执行任何检查。
expect(function() {
    // some piece of code which is expected to throw SomeException
})
->throws(SomeException::class)
->with(
        function(SomeException $e) { return null !== $e->getPrevious(); },
        'exception does have a previous exception'
);

当然,你也可以检查是否未发生特定异常

expect(function() {
    // some piece of code which is expected to not throw SomeException
})->doesNotThrow(SomeException::class);

通过省略异常名称,确保代码不会抛出任何异常

expect(function() {
    // some piece of code which is expected to not throw any exception
})->doesNotThrow();

如果这些期望中的任何一个失败,将抛出 AssertionFailure

对错误的期望

自版本 2.1.0 起可用

检查代码片段(例如函数或方法)是否触发错误

expect(function() {
    // some piece of code which is expected to trigger an error
})->triggers(E_USER_ERROR);

也可以通过省略错误级别来期望任何错误,而不仅仅是特定错误

expect(function() {
    // some piece of code which is expected to trigger an error
})->triggers();

此外,还可以对触发的错误执行额外检查

expect(function() {
    // some piece of code which is expected to trigger an error
})
->triggers(E_USER_WARNING)
->withMessage('some error occured');

以下是对异常的检查方法

  • withMessage(string $expectedMessage) 对错误消息执行 equals() 断言。
  • message($predicate) 使用给定的谓词对错误消息执行断言。

如果这些期望中的任何一个失败,将抛出 AssertionFailure

对代码执行后的状态的期望

有时,在执行某些代码片段后断言特定状态存在可能非常有用,无论执行是否成功。

expect(function() {
    // some piece of code here
})
->after(SomeClass::$value, equals(303));

可以将此与是否抛出异常的期望相结合

expect(function() {
    // some piece of code here
})
->doesNotThrow()
->after(SomeClass::$value, equals(303));

expect(function() {
    // some piece of code here
})
->throws(SomeException::class)
->after(SomeClass::$value, equals(303));

验证函数或方法的输出

自版本 2.1.0 起可用

当函数或方法使用 echo 时,检查它是否打印正确的输出可能很繁琐。为此,引入了 outputOf() 函数

outputOf(
        function() { echo 'Hello you!'; },
        equals('Hello world!')
);

第一个参数是一个可调用的函数,它打印一些输出,第二个参数是任何将应用于输出的谓词。outputOf() 负责启用和禁用输出缓冲区以捕获输出。

常见问题解答(FAQ)

我如何访问类或对象的属性进行断言?

与 PHPUnit 不同,bovigo/assert 不提供断言类属性是否满足特定约束的机制。如果属性是公开的,您可以将其直接作为值传递给 assertThat() 函数。在所有其他情况下,bovigo/assert 不支持访问受保护的或私有属性。它们之所以受保护或私有,有原因,测试应该仅针对类的公共 API,而不是它们的内部工作。