使用 phpspec 测试命令处理程序

Testing command handler with phpspec

最近我尝试使用 phpspec。它工作得很好,但我在测试命令处理程序时遇到了问题。例如在 PHPUnit 中我是这样测试的:

/**
 * @test
 */
public function it_should_change_an_email()
{
    $this->repository->add($this->employee);

    $this->handler->changeEmail(
        new ChangeEmailCommand(
            $this->employee->username()->username(),
            'new@email.com'
        )
    );

    Asserts::assertEquals(new Email('new@email.com'), $this->employee->email());
}

和设置:

protected function setUp()
{
    $this->repository = new InMemoryEmployeeRepository();
    $this->createEmployee();

    $this->handler = new EmployeeCommandHandler($this->repository);
}

要点是此测试对 Employee 对象进行断言以检查 CommandHandler 是否正常工作。但是在 phpspec 中,我不能对与指定对象不同的对象进行断言,在这种情况下,我只能对我的 CommandHandler 进行断言。那么如何在 phpspec 中测试命令处理程序?

编辑

也许间谍是要走的路:

class EmployeeCommandHandlerSpec extends ObjectBehavior
{
    const USERNAME = 'johnny';

    /** @var EmployeeRepository */
    private $employeeRepository;

    public function let(EmployeeRepository $employeeRepository)
    {
        $this->employeeRepository = $employeeRepository;
        $this->beConstructedWith($employeeRepository);
    }

    public function it_changes_the_employee_email(Employee $employee)
    {
        $this->givenEmployeeExists($employee);

        $this->changeEmail(
            new ChangeEmailCommand(self::USERNAME, 'new@email.com')
        );

        $employee->changeEmail(new Email('new@email.com'))->shouldHaveBeenCalled();
    }

    private function givenEmployeeExists(Employee $employee)
    {
        $this->employeeRepository->employeeWithUsername(new EmployeeUsername(self::USERNAME))
             ->shouldBeCalled()
             ->willReturn($employee);
    }
}    

员工 class 我已经指定了。因此,也许在命令处理程序中,只需检查 Employee 的方法是否已被调用就足够了。你怎么看待这件事?我的方向好吗?

消息传递

确实,您不应该验证状态,而是期待对象之间的某些交互。毕竟,这就是 OOP 的意义所在 - 消息传递。

您在 PHPUnit 中完成的方式是状态验证。它会强制您公开状态,因为您需要提供 "getter",这并不总是需要的。您感兴趣的是员工的电子邮件已更新:

$employee->updateEmail(new Email('new@email.com'))->shouldBeCalled();

如果您愿意,间谍也可以实现同样的效果:

$employee->updateEmail(new Email('new@email.com'))->shouldHaveBeenCalled();

Command/Query分离

我们通常只需要声明我们对有副作用的方法(命令方法从Command/Query分离)的期望。我们嘲笑他们。

查询方法不需要模拟,而是存根。您真的不希望 EmployeeRepository::employeeWithUsername() 应该被调用。这样做我们正在对实现做出假设,这反过来会使重构变得更加困难。你所需要的只是存根,所以 if 调用它的方法 returns 结果:

$employeeRepository->employeeWithUsername(new EmployeeUsername(self::USERNAME))
    ->willReturn($employee);

完整示例

class EmployeeCommandHandlerSpec extends ObjectBehavior
{
    const USERNAME = 'johnny';

    public function let(EmployeeRepository $employeeRepository)
    {
        $this->beConstructedWith($employeeRepository);
    }

    public function it_changes_the_employee_email(
        EmployeeRepository $employees, Employee $employee
    ) {
        $this->givenEmployeeExists($employees, $employee);

        $this->changeEmail(
            new ChangeEmailCommand(self::USERNAME, 'new@email.com')
        );

        $employee->changeEmail(new Email('new@email.com'))->shouldHaveBeenCalled();
    }

    private function givenEmployeeExists(
        EmployeeRepository $employees, Employee $employee
    ) {
        $employees->employeeWithUsername(new EmployeeUsername(self::USERNAME))
             ->willReturn($employee);
    }
}