webforge / object-asserter
用于对标量结构执行断言的流畅 DSL
Requires
- hamcrest/hamcrest-php: ^2.0
Requires (Dev)
- ergebnis/phpstan-rules: ^0.15.3
- phpstan/phpstan: ^0.12.92
- phpunit/phpunit: ^9.5
README
你有没有尝试过在 phpunit 中编写测试,这些测试断言了很多简单的属性,以及嵌套的数组和对象结构?
想象一下你得到了这个 JSON 结构
<?php $composerObject = (object) [ "name" => "webforge/object-asserter", "description" => "Fluent DSL to do assertions on scalar structures", "type" => "library", "require" => [ "hamcrest/hamcrest-php" => "^2.0" ], "require-dev" => [ "phpunit/phpunit" => "^9.5", "phpstan/phpstan" => "^0.12.92", "ergebnis/phpstan-rules" => "^0.15.3" ], "license" => "MIT", "autoload" => (object) [ "psr-4" => [ "Webforge\ObjectAsserter\\" => "src/" ] ], "autoload-dev" => (object)[ "psr-4" => [ "Webforge\ObjectAsserter\\" => "tests/" ] ], "authors" => [ (object)[ "name" => "Philipp Scheit", "email" => "p.scheit@ps-webforge.com" ] ], "minimum-stability" => "stable" ];
但你只想断言 composer.json 对象的某些部分。你已经学了很多关于断言的知识,并且知道如果断言 composer.json 等于我的测试文件会使得这个测试难以维护。如果你只想断言这部分怎么办?
然后你想要断言
- 结构是一个对象
- 它包含一个名为 name 的属性
- 这个属性是一个字符串
- 这个字符串包含某些内容
- 根对象有一个键 require
- 它是一个数组
- 带有字符串键
- 包含正则表达式
让我们看看,phpunit 代码会是什么样子
public function testThatMyComposerJsonIsCorrect_withPhpunit(): void { $composerObject = ... // same as above self::assertIsObject($composerObject); self::assertObjectHasAttribute('name', $composerObject); self::assertIsString($composerObject->name); self::assertStringContainsString('object-asserter', $composerObject->name); self::assertObjectHasAttribute('require', $composerObject); self::assertIsArray($composerObject->require); self::assertCount(1, $composerObject->require); self::assertArrayHasKey('hamcrest/hamcrest-php', $composerObject->require); self::assertSame('^2.0', $composerObject->require['hamcrest/hamcrest-php']); self::assertObjectHasAttribute('autoload-dev', $composerObject); self::assertObjectHasAttribute('psr-4', $composerObject->{'autoload-dev'}); self::assertIsArray($composerObject->{'autoload-dev'}->{'psr-4'}); self::assertCount(1, $composerObject->{'autoload-dev'}->{'psr-4'}); }
这不太好,不是吗?
但是...(总有但是,不是吗?)。这真的是一个好的测试吗?你能看懂吗?你能跟随它断言的内容吗?尝试运行它:它真的准确地传达了哪些失败了吗?
当我们编写测试时,我们通常很匆忙,我们忘记了“做正确的事”。例如,这里所有的缺失信息是什么?
$ phpunit docs/examples/Example.php
PHPUnit 9.5.6 by Sebastian Bergmann and contributors.
Runtime: PHP 7.4.15
Configuration: /app/phpunit.xml
.F 2 / 2 (100%)
Time: 00:00.003, Memory: 6.00 MB
There was 1 failure:
1) Example::testThatMyComposerJsonIsCorrect_withPhpunit
Failed asserting that an array has the key 'amcrest/hamcrest-php'.
/app/docs/examples/Example.php:23
好吧,所以我的一个断言失败了。但它在哪一部分?啊,让我打开这个测试,跳到第23行看看...不,不要这样做。做同事的善事,让他们知道出了什么问题,而不用查看测试内容。
让我们看看对象断言消息会是什么样子
$ phpunit docs/examples/Example.php
PHPUnit 9.5.6 by Sebastian Bergmann and contributors.
Runtime: PHP 7.4.15
Configuration: /app/phpunit.xml
FE 2 / 2 (100%)
Time: 00:00.005, Memory: 6.00 MB
There was 1 error:
1) Example::testThatMyComposerJsonIsCorrect_withObjectAsserter
Hamcrest\AssertionError: $root.require does not have key amcrest/hamcrest-php
Expected: array with key "amcrest/hamcrest-php"
but: array was ["hamcrest/hamcrest-php" => "^2.0"]
/app/vendor/hamcrest/hamcrest-php/hamcrest/Hamcrest/MatcherAssert.php:115
/app/vendor/hamcrest/hamcrest-php/hamcrest/Hamcrest/MatcherAssert.php:63
/app/src/ObjectAsserter.php:294
/app/src/ObjectAsserter.php:160
/app/docs/examples/Example.php:39
不要误会我:我一直都是 phpunit 的粉丝,但这些 Hamcrest 消息(我用于断言的基础库)是更好的。
注意: $root.require
没有键 amcrest/hamcrest-php。这就是你告诉正在调试失败的测试的同事:嘿,composer.json 中的 require 属性已更改并且有拼写错误。
现在,让我们看看如何使用 object-asserter 编写这个测试
public function testThatMyComposerJsonIsCorrect_withObjectAsserter(): void { $composerObject = $this->getComposerJsonDecoded(); $this->assertThatObject($composerObject) ->property('name')->contains('object-asserter')->end() ->property('require')->isArray()->length(1) ->key("amcrest/hamcrest-php", "^2.0")->end() ->end() ->property('autoload-dev') ->property('psr-4')->isArray()->length(1)->end() ->end(); } protected function assertThatObject(stdClass $object): \Webforge\ObjectAsserter\ObjectAsserter { return new ObjectAsserter($object); }
你能读懂吗?是的,它很丑陋,有很多 end(),但至少你现在能读懂它了。相信我:我在到处使用它,人们都能“理解”它是如何工作的。而且我遵守了我的承诺,失败的断言消息很好,相信我;)
如何使用这个库
composer require --dev webforge/object-asserter
在你的 TestCase 中创建一些基础方法。或者你可以使用提供的 AssertionsTrait。 (查看示例)
API
在所有可以传递 mixed $matcher
的情况下,你可以传递一个 \Hamcrest\Matcher
或只是一个原始值,这将将其包装为 \Hamcrest\Matchers::equalTo($primitive)
。这样我们就能得到所有 hamcrest 断言 的力量。
property(string $name, mixed $matcher = null)
断言父对象是一个对象,并且它有一个名为 $name 的属性。如果设置了 $matcher,则将断言属性的值与匹配器进行比较。
key(string|int $index, mixed $matcher = null)
断言父对象是一个包含键 $index 的数组,并且其值与 $matcher 匹配。
end()
当访问一个属性时,你现在处于该属性的值上下文中,允许你访问对象更深层次的链。使用end()可以返回上一级
$example = [ 'level1'=>[ 'level2'=>true ] ] $this->assertThatArray($example) ->key('level1') // now we do assertions on ['level2'=>true] ->end() // now we are back at the root: ['level1'=>['level2'=>true]]
contains(string $needle)
断言当前上下文是一个字符串,并且包含$needle
使用Hamcrest\Matchers::containsString()实现
length(int $length)
断言当前上下文是一个长度为$length的数组
debug()
将当前上下文通过var_dump()输出到控制台。当断言百万行长结构时,这对于穷人版的调试非常有用
get()
返回当前上下文并停止链式调用
tap(function($data, ObjectAsserter $objectAsserter))
在不停止链式调用的同时,访问当前上下文,第一个参数为$value,第二个参数为上下文
$this->assertThatObject($composerObject) ->property('authors') ->tap(function(array $authors) { // do whatever you like now, do phpunit assertions, or normalize, or, or self::assertArrayHasKey(0, $authors); }) ->key(0) // is still in context of the property authors
is(mixed $matcher), isNot(mixed $matcher)
对当前上下文进行断言。或对当前上下文进行否定断言
isNotEmptyString()
断言当前上下文是一个非空字符串
equals8601Date(\DateTimeInterface $expectedDate)
断言当前上下文是一个表示ISO8601日期的字符串,格式为Y-m-d
,并且与$expectedDate相等(同一天)。
properties(array $indexes, mixed $matcher): ObjectAsserter
断言当前上下文是一个数组,并且包含$indexes中的索引。
对于每个$index,它从这个索引获取数组中的值并与$matcher进行匹配。
用于快速胜利
$this->assertThatObject($composerObject) ->properties(['name', 'description', 'type'], Matchers::nonEmptyString());
更多示例
public function testThatMyValidationFactorReturnsTheRemoteAddress(): void { $response = json_decode(<<<'JSON' { "username" : "my_username", "password" : "my_password", "validation-factors" : { "validationFactors" : [ { "name" : "remote_address", "value" : "127.0.0.1" } ] } } JSON ); $factor0 = $this->assertThatObject($response) ->property('username')->contains('my_')->end() ->property('validation-factors') ->property('validationFactors')->isArray() ->key(0) ->property('name')->is(\Hamcrest\Matchers::equalToIgnoringCase('Remote_Address'))->end() ->get(); self::assertMatchesRegularExpression( '/(\d+\.){3}\d+/', $factor0->value, ); }
$this->assertThatObject($pbMeta = $binary->getMediaMetadata('focals.v1')) ->property('faces')->isArray()->length(2) // this might change and is okay, if algorithm improves ->key(0) ->property('confidence')->is(Matchers::greaterThan(0.4))->end() ->end() ->key(1) ->property('confidence')->is(Matchers::greaterThan(0.4))->end() ->end() ;
$this->assertJsonResponseContent(400, $this->client) ->property('detail', 'Nicht bestellbar: Die minimale Anzahl für dieses Format sind 13 Doppelseiten')->end() ->property('suggestion', 'Füge mehr Einträge hinzu.')->end(); $this->assertJsonResponseContent(403, $this->client) ->property('type', 'http://ps-webforge.net/rfc/redirect')->end() ->property('detail', Matchers::containsString('ist abgeschlossen'))->end() ->property('href', Matchers::containsString('photobook/' . $photobook->getId() . '/print'))->end();
享受编写测试的乐趣!
许可协议
MIT许可协议
版权所有 (c) 2021 webforge p.scheit@ps-webforge.com
特此授予任何获得本软件及其相关文档副本(“软件”)的人免费使用软件的权利,不受任何限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向软件提供者提供软件的人这样做,但须遵守以下条件
上述版权声明和本许可声明应包含在软件的副本或实质部分中。
软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性和非侵权性保证。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论这些责任是基于合同、侵权或其他方式,无论这些责任是否源于、因之而起或与软件或软件的使用或其他方式相关。