Php UnitTest对象模拟或真实对象
我和我的团队负责人讨论了关于UnitTest的问题, 在UnitTest中,我们是使用对象模拟还是使用真实对象? 我支持对象模拟的概念,因为我们应该只从对象输入/输出数据 最后,我们同意使用真实对象而不是模拟,所以下面是我的测试Php UnitTest对象模拟或真实对象,php,unit-testing,mocking,phpunit,Php,Unit Testing,Mocking,Phpunit,我和我的团队负责人讨论了关于UnitTest的问题, 在UnitTest中,我们是使用对象模拟还是使用真实对象? 我支持对象模拟的概念,因为我们应该只从对象输入/输出数据 最后,我们同意使用真实对象而不是模拟,所以下面是我的测试 <?php namespace App\Services\Checkout\Module\PaymentMethodRules; use App\Library\Payment\Method; use App\Services\Checkout\Module\
<?php
namespace App\Services\Checkout\Module\PaymentMethodRules;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
class AdminRule implements PaymentMethodRule
{
/**
* @var boolean
*/
private $isAdmin;
/**
* @var bool
*/
private $isBankTransferAvailable;
/**
* @param boolean $isAdmin
* @param bool $isBankTransferAvailable
*/
public function __construct($isAdmin, $isBankTransferAvailable)
{
$this->isAdmin = $isAdmin;
$this->isBankTransferAvailable = $isBankTransferAvailable;
}
/**
* @param PaymentMethodRuleManager $paymentMethodRuleManager
*/
public function run(PaymentMethodRuleManager $paymentMethodRuleManager)
{
if ($this->isAdmin) {
$paymentMethodRuleManager->getList()->add([Method::INVOICE]);
}
if ($this->isAdmin && $this->isBankTransferAvailable) {
$paymentMethodRuleManager->getList()->add([Method::BANK_TRANSFER]);
}
}
}
<?php
namespace tests\Services\Checkout\Module;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodList;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
use App\Services\Checkout\Module\PaymentMethodRules\AdminRule;
class AdminRuleTest extends \PHPUnit_Framework_TestCase
{
const IS_ADMIN = true;
const IS_NOT_ADMIN = false;
const IS_BANK_TRANSFER = true;
const IS_NOT_BANK_TRANSFER = false;
/**
* @test
* @dataProvider runDataProvider
*
* @param bool $isAdmin
* @param bool $isBankTransferAvailable
* @param array $expected
*/
public function runApplies($isAdmin, $isBankTransferAvailable, $expected)
{
$paymentMethodRuleManager = new PaymentMethodRuleManager(
new PaymentMethodList([]),
new PaymentMethodList([])
);
$adminRule = new AdminRule($isAdmin, $isBankTransferAvailable);
$adminRule->run($paymentMethodRuleManager);
$this->assertEquals($expected, $paymentMethodRuleManager->getList()->get());
}
/**
* @return array
*/
public function runDataProvider()
{
return [
[self::IS_ADMIN, self::IS_BANK_TRANSFER, [Method::INVOICE, Method::BANK_TRANSFER]],
[self::IS_ADMIN, self::IS_NOT_BANK_TRANSFER, [Method::INVOICE]],
[self::IS_NOT_ADMIN, self::IS_BANK_TRANSFER, []],
[self::IS_NOT_ADMIN, self::IS_NOT_BANK_TRANSFER, []]
];
}
}
对于这样一个通用问题的通用答案是:在进行单元测试时,您更喜欢使用尽可能多的“真实”代码。真实代码应该是默认值,模拟代码是异常
但当然,使用嘲笑有各种正当理由:
- “真实”代码在测试设置中不起作用
- 您还希望使用模拟框架来验证是否发生了某些操作
示例:您打算测试的代码调用某个远程服务(可能是数据库服务器)。当然,这意味着您需要一些进行端到端测试的测试。但是对于许多测试来说,不进行远程调用可能更方便;相反,您可以在这里使用mock,以避免远程数据库调用
或者,正如约翰·约瑟夫所建议的那样;您还可以从模拟所有/大多数依赖项开始;然后逐渐用真正的电话取代嘲笑。这个过程有助于将注意力集中在测试您真正想要测试的“部分”(而不是迷失在思考为什么使用“真正的其他代码”的测试会给您带来麻烦上)。IMHO我认为如果可以直接测试原始代码而不进行任何模拟会更好,因为这样会减少出错的可能性,我们可以避免这样的争论:如果被模仿的对象的行为与原始对象几乎相同,但我们不再生活在独角兽的世界里,那么模仿是一种必要的邪恶,还是不是?这仍然是个问题
因此,我想我可以将您的问题重新表述为何时使用dummy、fake、stub或mock?
通常,上述术语称为。
首先,您可以检查这个答案
有些情况下可能是好的:
- 被测对象/被测系统()有很多依赖项,这些依赖项是初始化所必需的,并且这些依赖项不会影响测试,因此这些依赖项可以是虚拟的
/**
* @inheritdoc
*/
protected function setUp()
{
$this->servicesManager = new ServicesManager(
$this->getDummyEntity()
// ........
);
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject
*/
private function getDummyEntity()
{
return $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods([])
->getMock();
}
- SUT具有外部依赖性,如基础设施/资源(例如,数据库、现金、文件…),因此,通过使用内存中的表示来伪造它是一种很好的方法,因为这样做的原因之一是避免将此基础设施/资源与测试数据混在一起
/**
* @var ArrayCollection
*/
private $inMemoryRedisDataStore;
/**
* @var DataStoreInterface
*/
private $fakeDataStore;
/**
* @inheritdoc
*/
protected function setUp()
{
$this->inMemoryRedisDataStore = new Collections\ArrayCollection;
$this->fakeDataStore = $this->getFakeRedisDataStore();
$this->sessionHandler = new SessionHanlder($this->fakeDataStore);
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject
*/
private function getFakeRedisDataStore()
{
$fakeRedis = $this->getMockBuilder(
Infrastructure\Memory\Redis::class
)
->disableOriginalConstructor()
->setMethods(['set', 'get'])
->getMock();
$inMemoryRedisDataStore = $this->inMemoryRedisDataStore;
$fakeRedis->method('set')
->will(
$this->returnCallback(
function($key, $data) use ($inMemoryRedisDataStore) {
$inMemoryRedisDataStore[$key] = $data;
}
)
);
$fakeRedis->method('get')
->will(
$this->returnCallback(
function($key) use ($inMemoryRedisDataStore) {
return $inMemoryRedisDataStore[$key];
}
)
);
}
- 当需要断言SUT的状态时,存根就变得很方便了。通常,这会与伪对象混淆,为了消除这一点,伪对象是帮助对象,它们永远不应该被断言
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
public function isSent(): bool;
}
/**
* Class SMSProviderStub
*/
class SMSProviderStub implements Provider\SMSProviderInterface
{
/**
* @var bool
*/
private $isSent;
/**
* @inheritdoc
*/
public function send()
{
$this->isSent = true;
}
/**
* @return bool
*/
public function isSent(): bool
{
return $this->isSent;
}
}
/**
* Class PaymentServiceTest
*/
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Service\PaymentService
*/
private $paymentService;
/**
* @var SMSProviderInterface
*/
private $smsProviderStub;
/**
* @inheritdoc
*/
protected function setUp()
{
$this->smsProviderStub = $this->getSMSProviderStub();
$this->paymentService = new Service\PaymentService(
$this->smsProviderStub
);
}
/**
* Checks if the SMS was sent after payment using stub
* (by checking status).
*
* @param float $amount
* @param bool $expected
*
* @dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount, bool $expected)
{
$this->paymentService->pay($amount);
$this->assertEquals($expected, $this->smsProviderStub->isSent());
}
/**
* @return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should return true' => [
'amount' => 28.99,
'expected' => true,
],
];
}
/**
* @return Provider\SMSProviderInterface
*/
private function getSMSProviderStub(): Provider\SMSProviderInterface
{
return new SMSProviderStub();
}
}
- 如果应该检查SUT的行为,那么mock很可能会起到解救作用,或者stubs(),它可以被检测到,因为很可能没有找到assert语句。例如,可以将mock设置为调用值为a的X方法,而b返回值Y,或者期望调用一个方法一次或N次,等等
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
}
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Service\PaymentService
*/
private $paymentService;
/**
* @inheritdoc
*/
protected function setUp()
{
$this->paymentService = new Service\PaymentService(
$this->getSMSProviderMock()
);
}
/**
* Checks if the SMS was sent after payment using mock
* (by checking behavior).
*
* @param float $amount
*
* @dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount)
{
$this->paymentService->pay($amount);
}
/**
* @return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should check behavior' => [
'amount' => 28.99,
],
];
}
/**
* @return SMSProviderInterface
*/
private function getSMSProviderMock(): SMSProviderInterface
{
$smsProviderMock = $this->getMockBuilder(Provider\SMSProvider::class)
->disableOriginalConstructor()
->setMethods(['send'])
->getMock();
$smsProviderMock->expects($this->once())
->method('send')
->with($this->anything());
}
}
角落案例
- SUT有很多依赖于其他事物的依赖项,为了避免这种依赖循环,因为我们只对测试某些方法感兴趣,可以模拟整个对象,但是可以将调用转发给原始方法
$testDouble = $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods(null);
IMHO您应该模拟每个未测试的对象,这样其他类上的每个更改都不会反映到单元测试中。也就是说,如果您更改PaymentMethodRuleManager或PaymentMethodList类的实现,您的测试将继续工作(为什么会中断?+1)。我还想补充一点,从模拟依赖项开始,并在最终实际创建真实对象时将其切换到真实对象是很有用的。这可以避免您不去创建对象而失去对当时正在测试的内容的关注,并且您可以继续您的主题测试,而不会使依赖关系模糊设计过程。@JohnJoseph这是一个有趣的方面-非常感谢;我在回答中加了这个。