robvanaarle/php-object-seam

一种简单的方法,通过最小化代码更改来创建对象缝隙,从而在测试遗留PHP代码时打破依赖关系

v1.1.1 2024-05-13 09:52 UTC

This package is not auto-updated.

Last update: 2024-09-30 11:46:42 UTC


README

PHP Object Seam 提供了一种简单的方法来在PHP中创建对象缝隙。这些用于遗留代码,以最小更改打破依赖关系,以便测试受测试的类。

由于依赖关系,遗留代码难以扩展和维护。理想情况下,它需要重构,但并不总是有时间这样做。为了对遗留代码的新功能或错误修复有信心,需要自动测试。但是遗留代码往往没有(足够的)自动测试。由于相同的依赖关系,这些测试也难以添加。为了放置测试,必须首先通过更改代码来打破依赖关系。这些更改也需要测试,但由于相同的原因,这很困难。

在他的书《有效地与遗留代码协作》中,Michael Feathers 定义了一个缝隙为“一个可以改变程序行为而不改变代码的地方”。这使打破依赖关系和添加自动化测试成为可能,无需或仅进行最小代码更改。这个库提供了一种创建缝隙类型的方法:对象缝隙。使用对象缝隙,可以在自动化测试中更改受测试的对象,同时保持受测试的类的代码不变。

安装

composer require --dev robvanaarle/php-object-seam ^1

需求

PHP >= 7.0

由于遗留代码通常在较旧的PHP版本上运行,因此此软件包旨在支持尽可能多的PHP版本。

特性

  • 调用受保护的和私有方法
  • 调用受保护的静态方法
  • 重写公共和受保护的类方法
  • 重写公共和受保护的静态方法
  • 使用自定义构造函数实例化对象
  • 捕获和检索公共和受保护的类方法调用
  • 捕获和检索公共和受保护的静态方法调用
  • 当使用 CreatesObjectSeams 特性时,在PhpStorm中进行自动完成
  • 测试框架无关

这允许使用《有效地与遗留代码协作》一书中的以下依赖关系打破技术。

  • 子类化并公开
  • 子类化并重写
  • 公开静态方法

手动创建对象缝隙代码的优势

  • 代码更少:大部分所需的代码都是自动生成的
  • 更快地编写
  • 更明确地表明打破依赖关系的意图:手动创建的对象缝隙代码往往变得模糊
  • ObjectSeam 可以在测试之前部分构建,并针对特定测试进行更改(甚至构建)

基本用法

在测试类中使用特性 PHPObjectSeam\CreatedObjectSeams 来创建 ObjectSeams。通常为受测试的对象创建 ObjectSeams。然后可以对其进行更改,而无需或仅对受测试的类进行少量代码更改,例如调用非公共方法或重写方法行为。创建的 ObjectSeam 是未构造的:原始构造函数,__construct,尚未调用。这允许设置可以由多个测试重用和定制的 ObjectSeam

class FooTest
{
    use PHPObjectSeam\CreatesObjectSeams;
    
    public function testBar(): void
    {
        $foo = $this->createObjectSeam(Foo::class);
        // $foo has type Foo&PHPObjectSeam\ObjectSeam
        
        // Access seam through seam() to alter the behaviour of the object
        $foo->seam()
          ->override('connect', fn ($username, $password) => 'dummy_token')
          ->customConstruct(function($arg1) {
              $this->url = 'http://www.dummy.url/' . $arg1;
          }, 'api/v1/');
          
        // do something with $foo and perform an assertion
    }
}

用法

调用非公共方法

$foo = $this->createObjectSeam(Foo::class);  
$result = $foo->seam()->call('nonPublicMethod', $arg1, $arg2);

这可以用于“子类化并公开”。

调用受保护的静态方法

$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->callStatic('protectedStaticMethod', $arg1, $arg2);

这可以用于“子类化并公开”。

重写公开或受保护的类方法

被重写的类方法将在对象连接类的作用域内执行。

使用闭包重写

$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->override('protectedMethod', function(int $arg1) {
  return $this->otherMethod($arg1) * 5;
});

使用结果值重写

$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->override('protectedMethod', 42);

这可以用于“子类化和重写”,目的是改变公开或受保护的类方法的行为。

重写公开或受保护的静态方法

被重写的静态方法将在对象连接类的作用域内执行。

使用闭包重写

$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->overrideStatic('protectedStaticMethod', function(int $arg1) {
  return parent::protectedMethod($arg1) * 3;
});

使用结果值重写

$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->overrideStatic('protectedStaticMethod', 9);

这可以用于“子类化和重写”,目的是改变公开或受保护的静态方法的行为。

使用自定义构造函数实例化对象

$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->customConstruct(function($arg1) {
    $this->url = 'http://www.dummy.url/' . $arg1;
}, 'api/v1/');

或者设置自定义构造函数并在之后调用它

// i.e. in the setup of your test
$this->foo = $this->createObjectSeam(Foo::class);
$this->foo->seam()->setCustomConstructor(function($arg1) {
    $this->url = 'http://www.dummy.url/' . $arg1;
});

// in a specific test case
$this->foo->callCustomConstructor('api/v1/');

不使用构造函数并调用所需方法可以实现“公开静态方法”,因此不需要将该方法设置为静态。

$this->foo = $this->createObjectSeam(Foo::class);
$this->foo->seam()->call('methodThatDoesNotUseThisKeyword');

调用原始构造函数

通常不需要自定义构造函数,那么就必须调用原始构造函数。

$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->call('__construct', 'bar');

或者使用辅助方法 callConstruct() 来实现

$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->callConstruct('bar');

捕获和检索公共和受保护的类方法调用

捕获和检索调用允许断言方法已被调用以及调用的参数。

$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->captureCalls('publicOrProtectedMethod');

// do something with $foo
$foo->publicMethod();

$calls = $foo->seam()->getCapturedCalls('publicOrProtectedMethod');
// assert that $calls contains a certain combination of arguments.

捕获和检索公共和受保护的静态方法调用

捕获和检索静态调用允许断言方法已被调用以及调用的参数。

$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->captureStaticCalls('publicOrProtectedStaticMethod');

// do something with $foo
$foo::publicMethod();

$calls = $foo->seam()->getCapturedStaticCalls('publicOrProtectedMethod');
// assert that $calls contains a certain combination of arguments.