lanfix / aspect-mock
由Aspects驱动的实验性Mocking框架
Requires
- php: ^8.0
- lanfix/goaop-framework: ^4.0
- phpunit/phpunit: ^9.5
- symfony/finder: >=4.4 <6.0
Requires (Dev)
- codeception/codeception: ^4.1
- codeception/specify: ^1.4
- codeception/verify: ^2.1
This package is auto-updated.
Last update: 2024-08-28 17:20:53 UTC
README
AspectMock不是一个普通的PHP Mocking框架。凭借面向切面编程的强大功能和令人惊叹的Go-AOP库,AspectMock允许你在PHP代码中几乎对任何内容进行stub和mock!
文档 | 测试双重构建器 | 类代理 | 实例代理 | 函数代理
动机
PHP是一种不适合测试的语言。真的。你如何伪造time()
函数以在每次测试调用中产生相同的结果?是否有任何方式来stub一个类的静态方法?你能否在运行时重新定义一个类的方法?像Ruby或JavaScript这样的动态语言允许我们这样做。这些特性对测试至关重要。AspectMock来拯救!
每天都会在PHP中编写数千行未测试的代码。在大多数情况下,这些代码实际上并不差,但PHP没有提供测试它们的能力。你可能建议从头开始重写它,遵循测试驱动设计实践,并在可能的情况下使用依赖注入。对于稳定的运行代码来说,这应该是必要的吗?嗯,还有更好的浪费时间的方法。
使用AspectMock你可以对几乎任何OOP代码进行单元测试。PHP结合AOP融入了动态语言中我们长期缺少的特性。没有理由不测试你的代码。你不必从头开始重写它以使其可测试。只需安装AspectMock与PHPUnit或Codeception,并尝试编写一些测试。这真的非常简单!
特性
- 为静态方法创建测试双重。
- 为任何地方调用的类方法创建测试双重。
- 动态重新定义方法。
- 简单易懂的语法。
代码提案
允许stub和mock静态方法。
让我们在运行时重新定义静态方法并验证它们的调用。
<?php function testTableName() { $this->assertEquals('users', UserModel::tableName()); $userModel = test::double('UserModel', ['tableName' => 'my_users']); $this->assertEquals('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->assertEquals('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','jon@coltrane.com'); // magic method $this->assertEquals('jon', $user->getName()); $user->save(['name' => 'miles']); // ActiveRecord->save did not hit database $AR->verifyInvoked('save'); $this->assertEquals('miles', $user->getName()); } ?>
覆盖甚至标准PHP函数
<?php namespace demo; test::func('demo', 'time', 'now'); $this->assertEquals('now', time());
简洁美观
只需4个方法即可进行方法调用验证,一个方法用于定义测试双重
<?php function testSimpleStubAndMock() { $user = test::double(new User, ['getName' => 'davert']); $this->assertEquals('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类来完成重负载工作。通过在每个方法调用中引入切点,Go!可以拦截几乎任何对方法的调用。AspectMock是一个非常小的框架,仅由8个文件组成,利用了Go! AOP框架的力量。了解面向切面编程和Go!库本身。
要求
PHP >= 5.6 + Go! AOP要求
安装
1. 在您的composer.json中添加aspect-mock。
{
"require-dev": {
"codeception/aspect-mock": "*"
}
}
2. 将AspectMock作为依赖项与Go! AOP一起安装。
php composer.phar update
配置
将AspectMock\Kernel
类包含在您的测试引导文件中。
使用Composer的Autoloader
<?php include __DIR__.'/../vendor/autoload.php'; // composer autoload $kernel = \AspectMock\Kernel::getInstance(); $kernel->init([ 'debug' => true, 'includePaths' => [__DIR__.'/../src'] ]); ?>
如果您的项目使用Composer的Autoloader,那么这就是您需要开始的全部。
使用自定义Autoloader
如果您使用自定义Autoloader(如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。
无Autoloader
如果仍然不起作用...
在测试之前显式加载所有必需的文件
<?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目录路径,您应该指定appDircacheDir
是一个目录,其中可以存储更新的源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中使用
使用新创建的bootstrap
在您的phpunit.xml
配置中。同时禁用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!库有很好的文档,因此提交Pull Request非常简单。
致谢
关注@codeception以获取更新。
由Michael Bodnarchuk开发。
许可证:MIT。