m3m0r7/method-injector

该库增强了测试中对方法的模拟。

0.2.0 2020-04-19 07:32 UTC

This package is auto-updated.

Last update: 2024-09-19 17:26:16 UTC


README

MethodInjector是一个开源软件项目,为生成目标类中方法、字段和常量的测试替身提供强大的支持。例如,MethodInjector可以执行以下操作。

  • 用模拟函数或匿名函数替换方法中的函数,并执行它。
  • 重写指定字段的默认值
  • 通过重写常量的值进行测试
  • 模拟特定方法的返回值
  • 可以在方法执行的开始和结束处插入处理过程

MethodInjector解析原始类文件并重建类。因此,即使原始类不可继承(即定义了final),也可以轻松创建测试替身。然而,也可以继承原始类,并通过继承声明期望的类来创建测试替身。

文档

DEMO

快速入门

可以从以下位置安装。

composer require --dev m3m0r7/method-injector

如何使用?

简单示例

要使用MethodInjector创建测试替身,请执行以下操作

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->replaceFunction(
                                'date',
                                function (...$args) {
                                    return '2012-01-01';
                                }
                            );
                    }
                );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

调用factory方法返回一个MethodInjector实例。inspect的第一个参数是创建测试替身的类名,第二个参数可以指定创建验证和测试替身的条件,第三个参数可以指定是否继承原始类。您还可以调用多个inspect方法,一次创建多个类的测试替身。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$inspector = function (Inspector $inspector) {
    return $inspector
        ->methodGroup(
            '*',
            function (Condition $condition) {
                return $condition
                ->replaceFunction(
                    'date',
                    function (...$args) {
                        return '2012-01-01';
                    }
                );
        }
    );
};
$test
    ->inspect(
        Foo::class,
        $inspector
    )
    ->inspect(
        Bar::class,
        $inspector
    )
    ->patch();

$fooMock = $test->createMock(Foo::class);
$barMock = $test->createMock(Bar::class);

因为测试替身是在MethodInjector的专用命名空间中生成的,所以基本上不会污染全局命名空间。

限制替换方法

您还可以通过指定方法名来指定并限制Inspector类的MethodInjector要替换的方法。方法名不区分大小写,根据PHP规范。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    'test',
                    function (Condition $condition) {
                        return $condition
                            ->replaceFunction(
                                'date',
                                function (...$args) {
                                    return '2012-01-01';
                                }
                            );
                }
            );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

替换类中的字段

您可能想重写类中字段的默认值。您还可以使用replaceField来更改字段的默认值。当然,即使字段是privateprotected,也可以更改字段。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->replaceField('testField', 'changed default value');
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

echo $mock->testField;

替换类中的常量

您可以使用replaceConstant以与重写字段相同的方式重写常量值。当然,这也适用于private,甚至protected

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->replaceConstant('TEST', 'changed default value');
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

echo $mock::TEST;

在开始处理方法时输出某些内容

您可能想在方法执行开始时中断一些处理。使用MethodInjector,您可以指定Condition类的before。在执行开始时,可以中断一些处理。例如,当您想测量单个方法的执行时间时,这很有用。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->before(function () {
                                echo "Hello HEAD!\n";
                            });
                }
            );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

在完成处理方法时输出某些内容

您也可以指定方法执行的结束,就像开始一样。当它完成后,将调用after

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->after(function () {
                                echo "Finish to run.\n";
                            });
                }
            );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

替换方法中的类

MethodInjector 还可以使用 replaceInstance 方法替换方法中实例化的类为另一个类。这允许使用还在开发中的用例,或者当你想要替换一个类为另一个类时。你还可以在读取时使用 staticself 来替换它。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->replaceInstance(
                                Foo::class,
                                Bar::class
                            );
                }
            );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

当然,即使你正在读取一个类的静态方法,你也可以使用 replaceStaticCall 来替换它。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->replaceStaticCall(
                                Foo::class,
                                Bar::class
                            );
                }
            );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

模拟方法

当方法本身还在开发中,或者当你想要测试一个返回特定值的测试,或者它不在你的测试兴趣范围内时,你可能想要创建一个测试替身。使用 MethodInjector,你也可以通过 replaceMethod 创建一个用于该方法本身的测试替身。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->replaceMethod(
                    'testMethod',
                    function () {
                        return 'Fixed value.';
                    }
                );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

添加替换器

MethodInjector 预先提供了几个替换器,但根据情况你可能想要添加更多的替换器。在这种情况下,你也可以添加替换器。默认情况下,MethodInjector 的替换器是设置为默认的,但如果添加了原始的替换器,请注意,MethodInjector 提供的播放器默认情况下不会被使用,所以它应该作为参数重新指定。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory([
    'replacer' => [
        [Inspector::FUNCTION, MyFunctionReplacer::class],
        [Inspector::CLASS_CONSTANT, MyConstantReplacer::class],
    ],
]);
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->replaceMethod(
                    'testMethod',
                    function () {
                        return 'Fixed value.';
                    }
                );
        }
    )
    ->patch();

$mock = $test->createMock(Test::class);

如果你使用 addReplacer,你可以直接使用 MethodInjector 提供的替换器。替换器的作用类似于 reduce,按照指定的顺序应用于解析的 AST 节点。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->addReplacer(Inspector::FUNCTION, MyFunctionReplacer::class)
                ->addReplacer(Inspector::CLASS_CONSTANT, MyConstantReplacer::class);
        }
    )
    ->patch();

更改检查器

MethodInjector 的检查器只提供了所需的最基本功能,并且可能存在障碍,例如无法用于具有历史的项目。在这种情况下,你可能想要替换检查器本身以便进行测试。当然,你也可以实现原始的检查器。在这种情况下,你应该扩展由 MethodInjector 默认提供的 Inspector

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory([
    'inspectorClass' => MyInspector::class,
]);
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->addReplacer(Inspector::FUNCTION, MyFunctionReplacer::class)
                ->addReplacer(Inspector::CLASS_CONSTANT, MyConstantReplacer::class);
        }
    )
    ->patch();

模拟类或特质中定义的常量、字段和方法。

MethodInjector 对父类中定义的方法和字段以及特质进行静态分析,并将它们分解为 AST 并模拟它们。这个特性允许你专注于编写眼前的测试,而无需考虑在创建测试替身时如何模拟父类和特质的类方法。此外,由于 MethodInjector 递归地引用类,因此可以模拟父类以及父类的父类,或者如果是特质,则可以模拟特质中定义的特质。

<?php
require __DIR__ . '/vendor/autoload.php';

use MethodInjector\Condition;
use MethodInjector\Inspector;
use MethodInjector\MethodInjector;

$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->enableParentMock(true)
                ->enableTraitsMock(true);
        }
    )
    ->patch();

构建器

如果你正在使用 MethodInjector,你可能觉得创建模拟所需的代码量很长。因此,MethodInjector 提供了一个对象,一个名为 ConditionBuilder 的构建器类,以便更容易地创建对象。默认情况下,ConditionBuilder 模拟父类和所有特质的方法和属性。

<?php
use MethodInjector\Builder\ConditionBuilder;

$builder = ConditionBuilder::factory(Test::class)
    ->replaceFunction(
        'date',
        function (...$args) {
            return '9999-99-99';
        }
    )
    ->make()
    ->patch();

你可以编写上面的内容。上面的内容等同于以下内容。

<?php
$test = \MethodInjector\MethodInjector::factory();
$test
    ->inspect(
        Test::class,
        function (Inspector $inspector) {
            return $inspector
                ->methodGroup(
                    '*',
                    function (Condition $condition) {
                        return $condition
                            ->replaceFunction(
                                'date',
                                function (...$args) {
                                    return '9999-99-99';
                                }
                            );
                    }
                )
                ->enableParentMock(true)
                ->enableTraitsMock(true);
        }
    )
    ->patch();

ConditionBuilder 默认应用的方法范围是 *,即所有方法。然而,你可能只想模拟一些方法。在这种情况下,你可以使用 group 来指定要模拟的方法。

<?php
use MethodInjector\Builder\ConditionBuilder;

$builder = ConditionBuilder::factory(Test::class)
    ->group('doSomething')
    ->replaceFunction(
        'date',
        function (...$args) {
            return '9999-99-99';
        }
    )
    ->make()
    ->patch();

在这种情况下,所有 doSomething 中的 date 函数都被模拟了。

<?php
use MethodInjector\Builder\ConditionBuilder;

$builder = ConditionBuilder::factory(Test::class)
    ->group('doSomething1')
    ->replaceFunction(
        'date',
        function (...$args) {
            return '9999-99-99';
        }
    )
    ->group('doSomething2')
    ->replaceFunction(
        'date',
        function (...$args) {
            return '0000-00-00';
        }
    )
    ->make()
    ->patch();

在上面的情况下,使用 doSomething1 执行的日期将返回 9999-99-99,而使用 doSomething2 执行的日期将返回 0000-00-00

许可证

MIT