codeception/aspect-mock

此包已废弃,不再维护。未建议替代包。

由Aspects驱动的实验性Mocking框架

4.1.1 2021-12-18 14:10 UTC

README

AspectMock不是一个普通的PHP mocking框架。凭借面向方面编程的力量和出色的Go-AOP库,AspectMock允许你在PHP代码中几乎对任何内容进行stub和mock!

文档 | 测试双倍构建器 | 类代理 | 实例代理 | 函数代理

Actions Status Latest Stable Version Total Downloads Monthly Downloads StandWithUkraine

动机

PHP是一种不是为可测试性而设计的语言。真的。你如何模拟time()函数以在每个测试调用中产生相同的结果?是否有任何方式可以stub一个类的静态方法?你能否在运行时重新定义一个类方法?像Ruby或JavaScript这样的动态语言允许我们这样做。这些功能对于测试是必不可少的。AspectMock来拯救!

每天都有数以千行计的PHP代码没有被测试。在大多数情况下,这些代码实际上并不差,但PHP没有提供测试这些代码的能力。你可能建议从头开始重写它,遵循测试驱动设计实践,并在可能的地方使用依赖注入。是否应该对稳定运行的代码这样做?嗯,有更好的浪费时间的方法。

使用AspectMock,你可以测试几乎任何OOP代码。PHP与AOP结合,纳入了我们长期缺少的动态语言特性。没有理由不测试你的代码。你不需要从头开始重写它使其可测试。只需安装AspectMock与PHPUnit或Codeception,然后尝试编写一些测试。这真的非常简单!

特性

  • 静态方法创建测试双倍。
  • 为在任何地方调用的类方法创建测试双倍。
  • 动态重新定义方法。
  • 简单语法,易于记忆。

代码提案

允许模拟静态方法。

让我们重新定义静态方法并在运行时验证它们的调用。

<?php

function testTableName()
{
	$this->assertSame('users', UserModel::tableName());	
	$userModel = test::double('UserModel', ['tableName' => 'my_users']);
	$this->assertSame('my_users', UserModel::tableName());
	$userModel->verifyInvoked('tableName');	
}

允许替换类方法。

测试使用ActiveRecord模式开发的代码。ActiveRecord模式的使用听起来像是不良实践?不。但下面的代码在经典单元测试中是不可测试的。

<?php

class UserService {
    function createUserByName($name) {
    	$user = new User;
    	$user->setName($name);
    	$user->save();
    }
}

没有AspectMock,你需要将User作为显式依赖项引入到类UserService中,以便对其进行测试。但是,让我们保留代码不变。它工作。尽管如此,我们仍然应该测试它以避免回归。

我们不希望$user>save方法实际执行,因为它将击中数据库。相反,我们将用模拟项替换它,并验证它是否被createUserByName调用。

<?php

function testUserCreate()
{
	$user = test::double('User', ['save' => null]);
	$service = new UserService;
	$service->createUserByName('davert');
	$this->assertSame('davert', $user->getName());
	$user->verifyInvoked('save');
}

拦截父类方法和魔术方法

<?php

// User extends ActiveRecord
function testUserCreate()
{
	$AR = test::double('ActiveRecord', ['save' => null]));
	test::double('User', ['findByNameAndEmail' => new User(['name' => 'jon'])])); 
	$user = User::findByNameAndEmail('jon','[email protected]'); // magic method
	$this->assertSame('jon', $user->getName());
	$user->save(['name' => 'miles']); // ActiveRecord->save did not hit database
	$AR->verifyInvoked('save');
	$this->assertSame('miles', $user->getName());
}

覆盖标准PHP函数

<?php

namespace demo;
test::func('demo', 'time', 'now');
$this->assertSame('now', time());

简洁漂亮

只需4个方法即可进行方法调用验证,一个方法用于定义测试双倍

<?php

function testSimpleStubAndMock()
{
	$user = test::double(new User, ['getName' => 'davert']);
	$this->assertSame('davert', $user->getName());
	$user->verifyInvoked('getName');
	$user->verifyInvokedOnce('getName');
	$user->verifyNeverInvoked('setName');
	$user->verifyInvokedMultipleTimes('setName',1);
}

检查方法setName是否以davert为参数调用。

<?php
$user->verifyMethodInvoked('setName', ['davert']);

哇!但它是如何工作的呢?

无需PECL扩展。Go! AOP库通过动态修补自动加载的PHP类来完成繁重的工作。通过引入切点(pointcuts)到每个方法调用中,Go!允许拦截几乎所有对方法的调用。AspectMock是一个非常小巧的框架,仅由8个文件组成,利用了Go! AOP框架的力量。查看面向切面编程和Go!库本身。

要求

  • PHP 7.4.
  • Go! AOP 3.0

安装

1. 在您的composer.json中添加aspect-mock。

{
	"require-dev": {
		"codeception/aspect-mock": "*"
	}
}

2. 使用Go! AOP作为依赖项安装AspectMock。

php composer.phar update

配置

AspectMock\Kernel类包含到您的测试引导文件中。

使用Composer的自动加载器

<?php

include __DIR__.'/../vendor/autoload.php'; // composer autoload

$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'debug' => true,
    'includePaths' => [__DIR__.'/../src']
]);

如果您的项目使用Composer的自动加载器,那么您就可以开始使用了。

使用自定义自动加载器

如果您使用自定义自动加载器(如Yii/Yii2框架),应明确指示AspectMock修改它

<?php

include __DIR__.'/../vendor/autoload.php'; // composer autoload

$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'debug' => true,
    'includePaths' => [__DIR__.'/../src']
]);
$kernel->loadFile('YourAutoloader.php'); // path to your autoloader

以这种方式加载项目的所有自动加载器,如果您不完全依赖于Composer。

不使用自动加载器

如果它仍然不起作用...

在测试之前显式加载所有必需的文件

<?php

include __DIR__.'/../vendor/autoload.php'; // composer autoload

$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'debug' => true,
    'includePaths' => [__DIR__.'/../src']
]);
require 'YourAutoloader.php';
$kernel->loadPhpFiles('/../common');

定制化

有一些选项可以自定义设置AspectMock。它们都在Go!框架中定义。如果AspectMock在您的项目中仍未运行,它们可能会有所帮助。

  • appDir定义正在测试的Web应用程序的根目录。根目录之外的所有类都将被AspectMock生成的代理类替换。默认情况下,它是位于composer的vendor目录中的目录。如果您不使用Composer或者您有自定义的composer的vendor文件夹路径,您应指定appDir
  • cacheDir是一个目录,更新的源PHP文件可以存储在其中。如果未设置此目录,则代理类将在每次运行时构建。否则,所有在测试中使用的PHP文件都将使用切面注入进行更新,并存储在cacheDir路径中。
  • includePaths是指定由Go Aop增强的文件所在的目录。应指向您的应用程序源文件以及框架文件和您使用的任何库。
  • excludePaths是不应受切面影响的PHP文件所在的路径。您应排除您的测试文件以避免拦截

示例

<?php

$kernel = \AspectMock\Kernel::getInstance();
$kernel->init([
    'appDir'    => __DIR__ . '/../../',
    'cacheDir'  => '/tmp/myapp',
    'includePaths' => [__DIR__.'/../src']
    'excludePaths' => [__DIR__] // tests dir should be excluded
]);

针对不同框架的更多配置.

正确配置AspectMock非常重要。否则,它可能不会按预期工作或产生副作用。请确保您包含了所有需要模拟的文件,但同时也排除了您的测试文件以及测试框架。

在PHPUnit中的使用

在您的phpunit.xml配置中使用新创建的bootstrap,并禁用backupGlobals

<phpunit bootstrap="bootstrap.php" backupGlobals="false">

在测试之间清除测试替身注册表。

<?php

use AspectMock\Test as test;

class UserTest extends \PHPUnit_Framework_TestCase
{
    protected function tearDown()
    {
        test::clean(); // remove all registered test doubles
    }

    public function testDoubleClass()
    {
        $user = test::double('demo\UserModel', ['save' => null]);
        \demo\UserModel::tableName();
        \demo\UserModel::tableName();
        $user->verifyInvokedMultipleTimes('tableName',2);
    }

在Codeception中的使用。

AspectMock\Kernel包含到tests/_bootstrap.php中。我们建议从您的CodeHelper类中包含对test::clean()的调用

<?php

namespace Codeception\Module;

class CodeHelper extends \Codeception\Module
{
	function _after(\Codeception\TestCase $test)
	{
		\AspectMock\Test::clean();
	}
}

改进?

肯定有改进的空间。这个框架并不是为了做你所有可能需要的任何事情而设计的(请参阅下面的注释)。但是,如果您觉得您需要一个特性,请提交一个Pull Request。由于代码不多,并且Go!库有很好的文档,所以这很容易。

致谢

关注@codeception获取更新。

Michael Bodnarchuk开发。

许可:MIT

Go! Aspect-Oriented Framework提供支持