webforge/object-asserter

用于对标量结构执行断言的流畅 DSL

1.1.0 2021-08-20 03:31 UTC

This package is auto-updated.

Last update: 2024-09-20 09:37:29 UTC


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

特此授予任何获得本软件及其相关文档副本(“软件”)的人免费使用软件的权利,不受任何限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向软件提供者提供软件的人这样做,但须遵守以下条件

上述版权声明和本许可声明应包含在软件的副本或实质部分中。

软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性和非侵权性保证。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论这些责任是基于合同、侵权或其他方式,无论这些责任是否源于、因之而起或与软件或软件的使用或其他方式相关。