tototoshi / staticmock
一个用于测试中替换静态方法的mock-like DSL。
Requires (Dev)
- herrera-io/runkit-hint: *
- phpunit/phpunit: ^9.5.3
Suggests
- ext-runkit: https://github.com/runkit7/runkit7
- dev-main
- 4.0.1
- 4.0.0
- 3.3.0
- 3.2.0
- 3.1.0
- 3.0.0
- 2.x-dev
- 2.0.0
- 1.x-dev
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- 0.2.1
- 0.2.0
- 0.1.0
- dev-dependabot/composer/phpstan/phpstan-1.12.3
- dev-dependabot/composer/phpstan/phpstan-1.12.0
- dev-dependabot/composer/phpunit/phpunit-9.6.19
- dev-try-hhvm
- dev-bad_usage_test
This package is not auto-updated.
Last update: 2024-09-12 15:48:31 UTC
README
一个用于测试中替换静态方法的mock-like DSL。
$mock = StaticMock::mock('FooService'); $mock ->shouldReceive('find') ->with(1) ->once() ->andReturn('Something'); $mock->assert();
动机
Mockery (https://github.com/padraic/mockery) 提供了创建mock对象的良好接口。但对于静态方法,Mockery需要一个别名类,我们无法使用它的简单DSL一次性创建一个mock对象。
StaticMock为静态方法提供了Mockery-like DSL。StaticMock依赖于runkit7扩展或uopz扩展,并在运行时临时重写静态方法。
要求
- PHP 7.3 和 runkit7/runkit7 1.0.11
- PHP >= 7.3 和 runkit7/runkit7 >= 4.0.0a3
- PHP >= 7.3 和 krakjoe/uopz
关于runkit7设置
请注意,runkit7的版本不同,其扩展名也不同。过去它是extension=runkit.so
,但现在它是extension=runkit7.so
安装
composer.json
{ "require-dev": { "tototoshi/staticmock": "4.0.2" } }
示例
假设你正在编写对User
类的测试,如下所示。
存根和mocking
class User { private $email; public function __construct($email) { $this->email = $email; } public function getFeed() { $g_feed = GooglePlusClient::getFeed($this->email, 1); $f_feed = FacebookClient::getFeed($this->email, 1); return array_merge($g_feed, $f_feed); } } class GooglePlusClient { public static function getFeed($email, $limit) { // send a request to Google } } class FacebookClient { public static function getFeed($email, $limit) { // send a request to Facebook } }
User
类有一个getFeed
方法。此方法从Google+和Facebook聚合用户的feed。它依赖于GooglePlusClient
和FacebookClient
从它们的API中获取feed。我们有时希望对GooglePlusClient
和FacebookClient
进行存根,以便对User
类进行测试。我们的目标是确保User
类可以正确地从API中聚合feed。现在我们不再关心GooglePlusClient
和FacebookClient
的行为。
问题是GooglePlusClient::getFeed
和FacebookClient::getFeed
是静态方法。如果它们是实例方法,我们可以管理它们的依赖关系,并将它们的存根注入到User
类中。但由于它们是静态方法,我们无法这样做。
StaticMock
通过在运行时临时替换方法来解决此问题。它提供了一个简单的DSL来替换方法。你只需要学习几个方法。
- 使用
StaticMock::mock
和shouldReceive
声明我们想要替换的方法。 - 使用
andReturn
定义方法的返回值。
见下文。在这个例子中,GooglePlusClient::getFeed
和FacebookClient::getFeed
被修改为返回array("From Google+")
和array("From Facebook")
。
class UserTest extends \PHPUnit\Framework\TestCase { public function testGetFeed() { $gmock = StaticMock::mock('GooglePlusClient'); $fmock = StaticMock::mock('FacebookClient'); $gmock->shouldReceive('getFeed')->andReturn(array("From Google+")); $fmock->shouldReceive('getFeed')->andReturn(array("From Facebook")); $user = new User('foo@example.com'); $this->assertEquals(array('From Google+', 'From Facebook'), $user->getFeed()); } }
StaticMock
还有一些方法可以模拟mock对象。
never()
、once()
、twice()
和times($times)
用于检查它们被调用的次数。with
和withNthArg
用于检查调用时传递的参数。
class UserTest extends \PHPUnit\Framework\TestCase { public function testGetFeed() { $user = new User('foo@example.com'); $gmock = StaticMock::mock('GooglePlusClient'); $fmock = StaticMock::mock('FacebookClient'); $gmock ->shouldReceive('getFeed') ->once() ->with('foo@example.com', 1) ->andReturn(array("From Google+")); $fmock ->shouldReceive('getFeed') ->once() ->with('foo@example.co', 1) ->andReturn(array("From Facebook")); $this->assertEquals(array('From Google+', 'From Facebook'), $user->getFeed()); $gmock->assert(); $fmock->assert(); } }
常见陷阱
由于StaticMock是通过构造函数和析构函数魔术实现的,因此需要分配mock变量($mock = StaticMock::mock('MyClass')
)。当通过StaticMock::mock
创建Mock
类的实例时,将替换方法,当实例超出作用域时将还原。
因此,以下代码不会按预期工作。
class UserTest extends \PHPUnit\Framework\TestCase { public function testGetFeed() { StaticMock::mock('GooglePlusClient') ->shouldReceive('getFeed') ->andReturn(array("From Google+")); StaticMock::mock('FacebookClient::getFeed') ->andReturn(array("From Facebook")); $user = new User('foo@example.com'); $this->assertEquals(array('From Google+', 'From Facebook'), $user->getFeed()); } }
替换方法实现
andImplement
非常有用,可以改变方法的行为。
再次查看下面。这次我们正在为User::register
编写测试,但我们不希望在每次运行测试时都发送电子邮件。
class User { private $email; public function __construct($email) { $this->email = $email; } public function register() { $this->save(); Mailer::send($this->email, 'Welcome to StaticMock'); } private function save() { echo 'save!'; } } class Mailer { public static function send($email, $body) { // send mail } }
传递一个如下所示的匿名函数。电子邮件不会发送,你将在控制台打印出一条简短的信息。
class UserTest extends \PHPUnit\Framework\TestCase { public function testRegister() { $mock = StaticMock::mock('Mailer'); $mock->shouldReceive('send')->andImplement(function () { echo "send email"; }); $user = new User('foo@example.com'); $user->register(); } }
使用PHPUnit
你可以轻松地定义自定义PHPUnit断言。请看以下内容。
use StaticMock\Mock; use StaticMock\PHPUnit\StaticMockConstraint; class WithPHPUnitTest extends \PHPUnit\Framework\TestCase { public function assertStaticMock(Mock $mock) { $this->assertThat($mock, new StaticMockConstraint); } }
许可证
BSD 3-Clause