interitty/phpunit

扩展了sebastianbergmann/phpunit框架。

v1.0.11 2024-08-30 12:02 UTC

This package is auto-updated.

Last update: 2024-08-30 10:03:05 UTC


README

扩展了sebastianbergmann/phpunit框架。

要求

安装

安装interitty/phpunit的最佳方式是使用Composer

composer require --dev interitty/phpunit

配置

该包使用了一个针对PHPStan的扩展来更好地预测通过反射获得的返回类型。如果已安装phpstan/extension-installer服务,则不需要进一步配置;否则,需要将以下配置添加到项目根目录中的phpstan.neon文件中。

includes:
    - ./vendor/interitty/phpunit/src/PHPStan/extension.neon

功能

而不是使用标准的PHPUnit\Framework\TestCase类,存在一个新的Interitty\PhpUnit\BaseTestCase类,它提供了一些其他功能。

附加断言

除了标准的断言之外,此扩展还提供了一些附加功能。

assertSameContent()

assertSameContent(iterable $expected, iterable $actual[, string $message]) 如果可迭代的$actual不包含与可迭代的$expected相同的内 容,则通过$message报告错误。

示例:使用assertArray()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class SameContentTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $content = [1, 2, 3, 4];
        $yield = static function ($content) {
            foreach ($content as $key => $value) {
                yield $key => $value;
            }
        };
        self::assertSameContent($content, $yield($content));
    }
}

expectExceptionCallback()

expectExceptionCallback(Closure $callback) 有时,可能需要将抛出的异常对象用于进一步的测试。因此,存在一个expectExceptionCallback扩展,允许您定义一个回调,其中异常作为参数可用。

示例:使用expectExceptionCallback()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Exceptions\Exceptions;
use LogicException;
use Throwable;

class ExceptionCallbackTest extends BaseTestCase
{
    public function testExtendTranslate(): void
    {
        $this->expectExceptionCallback(static function (Throwable $exception): void {
            self::assertSame('Message with key "foo"', (string) $exception);
        });

        throw Exceptions::extend(LogicException::class)
                ->setMessage('Message with key ":key"')
                ->addData('key', 'foo');
    }
}

expectExceptionData()

expectExceptionData([array $data]) Interitty\Utils扩展带来了Interitty\Exceptions\Exceptions::extend函数,该函数允许扩展任何异常以添加支持,例如存储额外的数据,这些数据也用于检索翻译后的描述。expectExceptionData扩展允许方便地验证这些数据。

示例:使用expectExceptionData()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Exceptions\Exceptions;
use LogicException;

class ExceptionDataTest extends BaseTestCase
{
    public function testExtendData(): void
    {
        $data = ['key' => 'foo'];
        $this->expectExceptionData($data);

        throw Exceptions::extend(LogicException::class)
                ->setMessage('Message with key ":key"')
                ->setData($data);
    }
}

工厂

Interitty\PhpUnit\BaseTestCase提供了一些有用的工厂。

createMockAbstract()

createMockAbstract(string $className[, array $methods, array $addMethods]) 工厂返回由$className定义的MockObject,并允许可选地模拟现有的$methods和不存在 的$addMethods

示例:使用createMockAbstract()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

abstract class FooClass
{
    abstract public function abstractMethod(): bool;

    public function isAccessible(): bool
    {
        return true;
    }
}

class MockAbstractTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $class = $this->createMockAbstract(FooClass::class, ['abstractMethod']);
        $class->expects(self::once())->method('abstractMethod')->willReturn(true);
        self::assertTrue($class->isAccessible());
        self::assertTrue($class->abstractMethod());
    }
}

createTempDirectory()

createTempDirectory([string $directoryName])虚拟文件系统存储中提供新的临时目录。

示例:使用createTempDirectory()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use function basename;

class CreateTempDirectoryTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $directoryName = 'directoryName';
        $tempDirectory = $this->createTempDirectory($directoryName);
        self::assertFileExists($tempDirectory);
        self::assertIsWritable($tempDirectory);
        self::assertSame($directoryName, basename($tempDirectory));
    }
}

createTempFile()

createTempFile([string $content, string $fileName])虚拟文件系统存储中提供新的临时文件。

示例:使用createTempFile()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Nette\Utils\FileSystem;

use function basename;

class CreateTempFileTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $content = 'Example of the file content';
        $fileName = 'tempFileName';
        $tempFile = $this->createTempFile($content, $fileName);
        self::assertFileExists($tempFile);
        self::assertIsWritable($tempFile);
        self::assertSame($content, FileSystem::read($tempFile));
        self::assertSame($fileName, basename($tempFile));
    }
}

数据提供者

Interitty\PhpUnit\BaseTestCase提供了一些标准数据提供者

alreadyDefinedStringDataProvider()

为测试字符串定义值的标准集合。

每个步骤包含参数:string $assertedData, string $message

参数描述
$assertedData断言的字符串
$message期望抛出的异常消息

示例请参阅文档中的以下部分

stringDataProvider()

测试字符串支持值的标准化值集。

每个步骤包含参数:string $assertedData

参数描述
$assertedData断言的字符串

使用示例请参考文档中的下方内容

unsupportedStringDataProvider()

测试字符串不支持值的标准化值集。

每个步骤包含参数:mixed $assertedData, string $message

参数描述
$assertedData断言数据
$message期望抛出的异常消息

使用示例请参考文档中的下方内容

辅助工具

Interitty\PhpUnit\BaseTestCase 提供了一些实用的辅助工具。

callNonPublicMethod()

callNonPublicMethod($object, string $methodName[, array $argument]) 提供了在 $object 上调用非公开方法 ($methodName) 并传递一些 $arguments 的能力。

示例:callNonPublicMethod() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class CallNonPublicMethodClass
{
    protected function isAccessible(): bool
    {
        return true;
    }
}

class CallNonPublicMethodTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $class = new CallNonPublicMethodClass();
        $isAccessible = $this->callNonPublicMethod($class, 'isAccessible');
        self::assertTrue($isAccessible);
    }
}
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class CallNonPublicMethodWithArgumentClass
{
    private function processData(string $data): string
    {
        return $data;
    }
}

class CallNonPublicMethodWithArgumentTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $data = 'test';
        $class = new CallNonPublicMethodWithArgumentClass();
        $result = $this->callNonPublicMethod($class, 'processData', [$data]);
        self::assertSame($data, $result);
    }
}

getNonPublicPropertyValue()

getNonPublicPropertyValue($object, string $propertyName) 提供了从 $object 中获取非公开属性 ($propertyName) 值的能力。

示例:getNonPublicPropertyValue() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetNonPublicPropertyClass
{
    /** @var bool */
    protected $propertyOne = true;

    /** @var bool */
    private $propertyTwo = true;
}

class GetNonPublicPropertyTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $someClass = new GetNonPublicPropertyClass();
        $propertyOne = $this->getNonPublicPropertyValue($someClass, 'propertyOne');
        $propertyTwo = $this->getNonPublicPropertyValue($someClass, 'propertyTwo');
        self::assertSame($propertyOne, $propertyTwo);
    }
}

setNonPublicPropertyValue()

setNonPublicPropertyValue($object, string $propertyName, mixed $value) 提供了在 $object 中设置非公开属性 ($propertyName) 值的能力。

示例:setNonPublicPropertyValue() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class SetNonPublicPropertyClass
{
    /** @var bool */
    protected $propertyOne = true;

    /** @var bool */
    private $propertyTwo = true;
}

class SetNonPublicPropertyTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $someClass = new SetNonPublicPropertyClass();
        $this->setNonPublicPropertyValue($someClass, 'propertyOne', false);
        $this->setNonPublicPropertyValue($someClass, 'propertyTwo', false);

        self::assertFalse($this->getNonPublicPropertyValue($someClass, 'propertyOne'));
        self::assertFalse($this->getNonPublicPropertyValue($someClass, 'propertyTwo'));
    }
}

processRegisterAutoload()

有时根据构造的字符串动态生成类/特质/接口可能很有用。为此,有一个辅助工具可以将传入的代码注册到自动加载器中。

示例:processRegisterAutoload() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Strings;
use Nette\PhpGenerator\Helpers;

use function class_exists;

class GeneratedCodeAutoloadTest extends BaseTestCase
{
    /** All available class / interface / namespace name constants */
    protected const NAME_NAMESPACE = 'Vendor\\Namespace';
    protected const NAME_DUMMY_CLASS = self::NAME_NAMESPACE . '\\DummyClass';

    public function testSuccess(): void
    {
        $className = self::NAME_DUMMY_CLASS;

        self::assertFalse(class_exists($className));
        $this->generateDummyClass($className);
        self::assertTrue(class_exists($className));
    }

    /**
     * Dummy class generator
     *
     * @param string $className
     * @return void
     */
    protected function generateDummyClass(string $className): void
    {
        $classShortName = Helpers::extractShortName($className);
        $namespace = Strings::before($className, '\\' . $classShortName, -1);
        $code = '<?php
declare(strict_types=1);

' . ((string) $namespace === '' ? '' : 'namespace ' . $namespace . ';') . '

class ' . $classShortName . ' {
}
';
        $this->processRegisterAutoload($className, $code);
    }
}

标准测试器

由于 getter 和 setter 格式的标准化,还可以为它们提供标准单元测试。

processTestGetBoolDefault()

processTestGetBoolDefault(string|string[]$className, string $propertyName, bool $default) 标准布尔 getter 默认值的测试器

示例:processTestGetBoolDefault() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetBoolDefaultClass
{

    /** @var bool */
    protected $foo = false;

    /**
     * Foo getter
     *
     * @return bool
     */
    protected function isFoo(): bool
    {
        return $this->foo;
    }
}

class GetBoolDefaultTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetBoolDefault(GetBoolDefaultClass::class, 'foo', false);
    }
}

processTestGetDefault()

processTestGetDefault(string|string[] $className, string $propertyName, mixed $default) 标准 getter 默认值的测试器

示例:processTestGetDefault() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetDefaultClass
{

    /** @var string */
    protected $foo = '';

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        return $this->foo;
    }
}

class GetDefaultTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetDefault(GetDefaultClass::class, 'foo', '');
    }
}

processTestGetSet()

processTestGetSet(string|string[] $className, string $propertyName, mixed $value) 标准 getter/setter 实现的测试器

示例:processTestGetSet() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetSetClass
{
    /** @var string */
    protected $foo = 'foo';

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        return $this->foo;
    }

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(string $foo)
    {
        $this->foo = $foo;
        return $this;
    }
}

class GetSetTest extends BaseTestCase
{
    /**
     * @dataProvider stringDataProvider
     */
    public function testSuccess(string $foo): void
    {
        $this->processTestGetSet(GetSetClass::class, 'foo', $foo);
    }
}

processTestGetSetBool()

processTestGetSetBool(string|string[] $className, string $propertyName, bool $value) 标准布尔 getter/setter 实现的测试器

示例:processTestGetSetBool() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetSetBoolClass
{
    /** @var bool */
    protected $foo = false;

    /**
     * Foo checker
     *
     * @return bool
     */
    protected function isFoo(): bool
    {
        return $this->foo;
    }

    /**
     * Foo setter
     *
     * @param bool $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(bool $foo)
    {
        $this->foo = $foo;
        return $this;
    }
}

class GetSetBoolTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetSetBool(GetSetBoolClass::class, 'foo', true);
    }
}

processTestGetUndefined()

processTestGetUndefined(string|string[] $className, string $propertyName, string $expectation) 标准 getter 为缺失必填值的测试器

示例:processTestGetUndefined() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class GetUndefinedClass
{
    /** @var string */
    protected $foo;

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        assert(Validators::check($this->foo, 'string', 'foo before get'));
        return $this->foo;
    }
}

class GetUndefinedTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetUndefined(GetUndefinedClass::class, 'foo', 'string');
    }
}

processTestSetAlreadyDefined()

processTestSetAlreadyDefined(string|string[]$className, string $propertyName, $value, string $message) 标准 setter 为覆盖已定义值的测试器

示例:processTestSetAlreadyDefined() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class SetAlreadyDefinedClass
{
    /** @var string */
    protected $foo;

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo($foo)
    {
        assert(Validators::check($this->foo, 'null', 'foo before set'));
        $this->foo = $foo;
        return $this;
    }
}

class SetAlreadyDefinedTest extends BaseTestCase
{
    /**
     * @dataProvider alreadyDefinedStringDataProvider
     */
    public function testSuccess(string $string, string $message): void
    {
        $this->processTestSetAlreadyDefined(SetAlreadyDefinedClass::class, 'foo', $string, $message);
    }
}

processTestSetAlreadyDefinedObject()

processTestSetAlreadyDefinedObject(string|string[] string $className, string $propertyName, object $value) 标准对象 setter 为覆盖已定义值的测试器

示例:processTestSetAlreadyDefinedObject() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class SetAlreadyDefinedObjectClass
{
    /** @var object */
    protected $foo;

    /**
     * Foo setter
     *
     * @param SetAlreadyDefinedObjectClass $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(SetAlreadyDefinedObjectClass $foo)
    {
        assert(Validators::check($this->foo, 'null', 'foo before set'));
        $this->foo = $foo;
        return $this;
    }
}

class SetAlreadyDefinedObjectTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $object = new SetAlreadyDefinedObjectClass();
        $this->processTestSetAlreadyDefinedObject(SetAlreadyDefinedObjectClass::class, 'foo', $object);
    }
}

processTestSetUnsupportedValue()

processTestSetUnsupportedValue(string|string[] $className, string $propertyName, mixed $value, string $message) 标准 setter 为插入不支持值的测试器

示例:processTestSetUnsupportedValue() 的使用
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class SetUnsupportedValueClass
{
    /** @var string */
    protected $foo;

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo($foo)
    {
        assert(Validators::check($foo, 'string', 'Foo'));
        $this->foo = $foo;
        return $this;
    }
}

class SetUnsupportedValueTest extends BaseTestCase
{
    /**
     * @dataProvider unsupportedStringDataProvider
     */
    public function testSuccess(mixed $foo, string $message): void
    {
        $this->processTestSetUnsupportedValue(SetUnsupportedValueClass::class, 'foo', $foo, $message);
    }
}

集成测试用例

Interitty\PhpUnit\BaseTestCase 类旨在用于“单元测试”,但有时需要与依赖注入容器一起工作。本包附带 Interitty\PhpUnit\BaseIntegrationTestCase 类,该类提供了一个 createContainer 工厂方法,有助于为每个测试需求生成特定的 DI 容器。

额外的集成工厂

Interitty\PhpUnit\BaseIntegrationTestCase 提供了一些有用的工厂。

createContainer()

createContainer(string $configFilePath) DI 容器工厂方法。

示例:createContainer() 的用法
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Nette\DI\MissingServiceException;

use function get_class;

class CreateContainerTest extends BaseIntegrationTestCase
{
    public function testSuccess(): void
    {
        $configContent = '
services:
    CreateContainerTest:
        class: Interitty\PhpUnit\CreateContainerTest
';
        $configFilePath = $this->createTempFile($configContent, 'config.neon');
        $container = $this->createContainer($configFilePath);

        self::assertInstanceOf(CreateContainerTest::class, $container->getByType(CreateContainerTest::class));

        $thrownException = null;
        try {
            $service = $container->getByType(CreateContainerTest::class);
            self::assertSame(CreateContainerTest::class, get_class($service));
        } catch (MissingServiceException $exception) {
            $thrownException = $exception;
        }
        self::assertNull($thrownException);
    }
}

BaseDibiTestCase

为了测试 SQL 数据库或使用 dibi/dibi 库,有一个 Interitty\PhpUnit\BaseDibiTestCase 类。

默认配置与内存中的 SQLite 兼容,也可以通过 setConfig() 方法进行更改。

数据库的结构和内容通过 setupDatabase 方法设置,如下例所示。

要访问 Dibi/Connection,只需使用同名的获取器 getConnection

<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Dibi\Connection;
use Dibi\Type;

use function iterator_to_array;

class DibiConnectionTest extends BaseDibiTestCase
{
    /**
     * @inheritdoc
     */
    protected function setupDatabase(Connection $connection): void
    {
        parent::setupDatabase($connection);
        $connection->query('create table Person (id int PRIMARY KEY, name varchar NOT NULL, active bool DEFAULT 1)');
        $connection->insert('Person', ['id' => 1, 'name' => 'test 1', 'active' => true])->execute();
        $connection->insert('Person', ['id' => 2, 'name' => 'test 2', 'active' => true])->execute();
        $connection->insert('Person', ['id' => 3, 'name' => 'test 3', 'active' => false])->execute();
    }

    // <editor-fold defaultstate="collapsed" desc="Integration tests">
    /**
     * Tester of dibi connection implementation
     *
     * @return void
     */
    public function testDibiConnection(): void
    {
        $expectedData = [
            ['id' => 1, 'name' => 'test 1', 'active' => true],
            ['id' => 2, 'name' => 'test 2', 'active' => true],
            ['id' => 3, 'name' => 'test 3', 'active' => false],
        ];

        $query = $this->getConnection()->select('*')->from('Person');

        $data = $query
            ->setupResult('setRowFactory', static function (array $data): array {
                return $data;
            })
            ->setupResult('setType', 'active', Type::BOOL)
            ->getIterator();

        self::assertSame($expectedData, iterator_to_array($data));
    }

    // </editor-fold>
}