tototoshi/staticmock

一个用于测试中替换静态方法的mock-like DSL。

4.0.1 2021-05-13 00:08 UTC

README

CI

一个用于测试中替换静态方法的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扩展,并在运行时临时重写静态方法。

要求

关于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。它依赖于GooglePlusClientFacebookClient从它们的API中获取feed。我们有时希望对GooglePlusClientFacebookClient进行存根,以便对User类进行测试。我们的目标是确保User类可以正确地从API中聚合feed。现在我们不再关心GooglePlusClientFacebookClient的行为。

问题是GooglePlusClient::getFeedFacebookClient::getFeed是静态方法。如果它们是实例方法,我们可以管理它们的依赖关系,并将它们的存根注入到User类中。但由于它们是静态方法,我们无法这样做。

StaticMock通过在运行时临时替换方法来解决此问题。它提供了一个简单的DSL来替换方法。你只需要学习几个方法。

  • 使用StaticMock::mockshouldReceive声明我们想要替换的方法。
  • 使用andReturn定义方法的返回值。

见下文。在这个例子中,GooglePlusClient::getFeedFacebookClient::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)用于检查它们被调用的次数。
  • withwithNthArg用于检查调用时传递的参数。
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