【问题标题】:PHPUnit method call expectations vs. assertionsPHPUnit 方法调用期望与断言
【发布时间】:2026-01-27 13:40:01
【问题描述】:

创建模拟类通常涉及在模拟/测试替身上配置方法调用预期。

例如在“香草”PHPUnit 中,我们可以存根方法调用并设置期望值,如下所示:

$stub->expects($this->any())->method('doSomething')->willReturn('foo');

Mockery模拟对象框架中,我们得到这样的API:

$mock->shouldReceive('doIt')->with(m::anyOf('this','that'))->andReturn($this->getSomething());

像这样的期望通常在测试套件的设置阶段被连接起来,例如\PHPUnit_Framework_TestCasesetUp()方法。

如果不能满足上述期望,就会破坏测试。因此,使期望成为实际的断言。

这导致我们的断言(断言 + 期望)分散在测试用例类周围,因为我们最终在测试用例的设置阶段以及单个测试中都有实际的断言。

在“常规”assert.. 方法中测试方法调用预期是否是一种好习惯。这可能看起来像这样(嘲笑):

public function setUp()
{
    $mock = m::mock(SomeClass::class);
    $mock->shouldReceive('setSomeValue');
    $this->mock = $mock;
}

以及稍后在其中一种测试方法结束时:

public function testSoemthing()
{ 
    ...
    $this->assertMethodCalled($this->mock, 'setSomeValue');
}

assertMethodCalled 不是 PHPUnit 公开的方法。它必须实施。

简而言之,我们是否应该将期望声明视为实际断言,然后在我们的测试方法中对其进行测试?

【问题讨论】:

    标签: unit-testing phpunit mockery


    【解决方案1】:

    您无需在 setUp() 方法中配置测试替身(存根/模拟)。

    实际上,我永远不会在测试设置代码中配置模拟,我只会在其中放置非常常见的存根。每个测试用例的模拟期望通常是不同的。我宁愿在设置代码中实例化我的测试替身,将它们分配给私有属性并在每个测试用例中配置它们。

    private $registration;
    private $repository;
    
    protected function setUp()
    {
        $this->repository = $this->getMock(UserRepository::class);
        $this->registration = new Registration($repository);
    }
    
    public function testUserIsAddedToTheRepositoryDuringRegistration()
    {
        $user = $this->createUser();
    
        $this->repository
             ->expects($this->once())
             ->method('add')
             ->with($user);
    
        $this->registration->register($user);
    }
    

    因此,可以将您的测试双重配置放入测试用例中。它实际上更好,因为您的测试用例具有所有上下文,因此更具可读性。如果你发现自己配置了很多测试替身或编写了很长的测试用例,这可能意味着你有太多的合作者,你应该考虑如何改进你的设计。您还可以使用具有有意义名称的辅助方法来使您的测试用例更具可读性 - 即 $this->givenExistingUser()$this->givenRepositoryWithNoUsers()

    正如您所注意到的,模拟期望(不要将它们与不是期望的存根混淆)非常接近断言。实际上,有一种方法可以通过另一种类型的测试替身 - 间谍来完成您正在寻找的事情。

    经典的 phpunit 模拟框架不支持间谍。但是,PHPUnit 现在已经内置了对预言的支持。幸运的是,预言supports spies。这是一个例子:

    private $registration;
    private $repository;
    
    protected function setUp()
    {
        $this->repository = $this->prophesize(UserRepository::class);
        $this->registration = new Registration($repository->reveal());
    }
    
    public function testUserIsAddedToTheRepositoryDuringRegistration()
    {
        $user = $this->createUser();
    
        $this->registration->register($user);
    
        $this->repository->add($user)->shouldHaveBeenCalled();
    }
    

    最后一点,我避免在同一个测试用例中使用断言和模拟期望。大多数情况下,这些都是不同的行为,应该由不同的测试用例覆盖。

    要了解更多关于测试替身的信息,请阅读优秀的PHP Test Doubles Patterns

    【讨论】:

    • 感谢您的宝贵意见。我还发现 Mockery 的 MockInterface 还公开了一个方法 shouldHaveBeenCalled(),因此它可以与您在 Prophecy 中展示的类似使用。此外,将预期的方法调用与其他行为分开测试确实会好得多。总之,我认为可以肯定地说shouldHaveBeenCalled() 是一个成熟的断言并需要它自己的测试用例。