为什么没有mock对象的测试就像做饭不放盐
\n\n想象一下,您正在编写一个发送电子邮件通知、操作数据库并调用第三方API的应用程序。如何在没有PHP中的mock对象和fixtures的情况下测试它?您将每次发送真实的邮件,在数据库中创建测试记录,并等待外部服务的响应。这很慢、不可靠,而且很愚蠢。
\n\n这时,test doubles——真实对象的替代品——就派上了用场。在PHP生态系统中,我们使用以下组合:mock对象用于检查交互,fixtures用于准备数据。让我们看看这在实践中是如何工作的。
\n\nMock vs Stub vs Fake:不要混淆概念
\n\n在深入代码之前,让我们先厘清概念。在PHP单元测试的世界中,有三种主要的替代品类型:
\n\n- \n
- Stub(桩)——返回预先定义的数据。您测试您的代码是否正确处理了这些数据。 \n
- Mock(模拟)——不仅返回数据,还检查您的系统如何调用方法:调用次数、参数、顺序。 \n
- Fake(伪造)——一个简化的工作实现(例如,内存数据库)。 \n
在PHPUnit(PHP测试的事实标准)中,这些概念经常被混淆。但我们要精确:PHP中的mock对象是指您设置期望(expectations)的那些。
\n\n在PHPUnit中编写第一个mock对象
\n\n假设我们有一个发送邮件的类Mailer。我们想验证注册服务是否恰好调用了一次send()方法,并且参数正确。
<?php\nuse PHPUnit\\Framework\\TestCase;\n\nclass UserRegistrationTest extends TestCase\n{\n public function testSendWelcomeEmailOnRegistration(): void\n {\n // 为Mailer创建mock\n $mailer = $this->createMock(Mailer::class);\n \n // 设置期望:send方法将被调用1次\n $mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->equalTo('user@example.com'),\n $this->stringContains('Welcome')\n );\n \n $service = new UserRegistrationService($mailer);\n $service->register('user@example.com', 'password123');\n }\n}\n\n\n注意:我们不检查邮件是否真的发送了。我们检查的是我们的代码尝试发送了它——这就是PHP测试中mock对象的本质。如果send()方法没有被调用(或被调用了两次),测试将失败。
expects方法及其功能
\n\nPHPUnit提供了丰富的API来设置期望:
\n\n- \n
$this->once()——恰好调用一次 \n $this->exactly(3)——恰好调用三次 \n $this->atLeastOnce()——至少调用一次 \n $this->never()——方法不应被调用 \n
对于参数,使用匹配器(constraints):
\n\n$mailer->expects($this->once())\n ->method('send')\n ->with(\n $this->callback(function($email) {\n return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;\n }),\n $this->anything() // 第二个参数我们无所谓\n );\n\n\nPHP中的Fixtures:在需要之前准备好数据
\n\nFixtures是测试执行前系统所处的预定义数据状态。在PHPUnit中,使用setUp()和