tototoshi/staticmock
Composer 安装命令:
composer require tototoshi/staticmock
包简介
A mockery-like DSL to replace static method in test.
README 文档
README
A mockery-like DSL to replace static methods in tests.
$mock = StaticMock::mock('FooService'); $mock ->shouldReceive('find') ->with(1) ->once() ->andReturn('Something'); $mock->assert();
Motivation
Mockery (https://github.com/padraic/mockery) provides nice interfaces to create mock objects. But as for static methods, Mockery needs an alias class and we can't create a mock object in one shot with his easy DSL.
StaticMock provides Mockery-like DSL for static methods. StaticMock depends on runkit7 extension or uopz extension and rewrites static methods temporarily at run-time.
Requirements
- PHP 7.3 and runkit7/runkit7 1.0.11
- PHP >= 7.3 and runkit7/runkit7 >= 4.0.0a3
- PHP >= 7.3 and krakjoe/uopz
About runkit7 settings
Please note that the extension name differs depending on the version of runkit7. In the past it was extension=runkit.so, but nowadays it is extension=runkit7.so
Install
composer.json
{ "require-dev": { "tototoshi/staticmock": "4.0.2" } }
Example
Imagine that you are writing tests for User class such as this.
Stubbing and 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 class has a getFeed method. This method aggregates user's feeds from Google+ and Facebook. It depends on GooglePlusClient and FacebookClient to fetch feeds from their API. We sometimes want stubs for GooglePlusClient and FacebookClient to write tests for the User class. Our goal is only to ensure that User class can correctly aggregate feeds from APIs. The behavior of GooglePlusClient and FacebookClient is out of our heads now.
The problem is GooglePlusClient::getFeed and FacebookClient::getFeed are static methods. If they were instance methods, we could manage their dependencies and inject stubs of them to User class. But since they are static methods, we can't do that.
StaticMock solved the problem by replacing the methods temporarily at run-time. It provides an easy DSL for replacing methods. All you need to learn is only a few methods.
- Declare the methods we want to replace with
StaticMock::mockandshouldReceive. - The return value of the method is defined with
andReturn.
See below. In this example, GooglePlusClient::getFeed and FacebookClient::getFeed are changed to return array("From Google+") and 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 also has some methods to act as mock objects.
never(),once(),twice()andtimes($times)are used to check how many times they are called.withandwithNthArgare used to check what arguments are passed when they are called.
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(); } }
Common pitfalls
Assigning a mock variable ($mock = StaticMock::mock('MyClass')) is required since StaticMock is implemented with constructor and destructor magic.
The methods are replaced when the instance of Mock class is created by StaticMock::mock and reverted when the instance goes out of scope.
So, the following code doesn't work as you expect.
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()); } }
Replacing method implementation
andImplement is useful to change the behavior of the method.
See below again. We are writing a test for User::register this time but we don't want to send an email every time running the test.
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 } }
Pass an anonymous function like below. The Email will not be sent and only a short line will be printed on your console.
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(); } }
With PHPUnit
You can easily define custom PHPUnit assertion. See below.
use StaticMock\Mock; use StaticMock\PHPUnit\StaticMockConstraint; class WithPHPUnitTest extends \PHPUnit\Framework\TestCase { public function assertStaticMock(Mock $mock) { $this->assertThat($mock, new StaticMockConstraint); } }
LICENSE
BSD 3-Clause
tototoshi/staticmock 适用场景与选型建议
tototoshi/staticmock 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.22M 次下载、GitHub Stars 达 44, 最近一次更新时间为 2013 年 10 月 08 日, 在 PHP 生态内属于活跃度较高的组件。
我们在过去多个企业项目中使用过 tototoshi/staticmock 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 tototoshi/staticmock 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
统计信息
- 总下载量: 1.22M
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 44
- 点击次数: 21
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: BSD-3-Clause
- 更新时间: 2013-10-08