我与我的团队负责人就UnitTest进行了讨论,问题是, 在UnitTest中,我们使用Object Mocking还是使用Real Object? 我支持Object Mocking概念,因为我们只应该从Objects输入/输出数据。
最后我们同意使用Real对象而不是Mocking,所以以下是我的测试
<?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, []]
];
}
}
我的问题是,在单元测试中应该使用真实对象还是对象模拟?为什么? 第二个问题,给定的单元测试在单元测试方面是对还是错。
答案 0 :(得分:2)
这个一般性问题的一般答案是:你更喜欢使用&#34;真实的&#34;进行单元测试时尽可能编码。 Real 代码应为默认,模拟代码为例外!
但是,当然,使用模拟:
有各种正当理由示例:您要测试的代码会调用某些远程服务(可能是数据库服务器)。当然,这意味着您需要一些进行端到端测试的测试。但是对于许多测试来说,不进行远程调用可能会更方便;相反,你会在这里使用模拟 - 避免远程数据库调用。
或者,正如John Joseph所建议的那样;你可能也开始模仿所有/大多数依赖项;然后逐渐用真实的电话取代模拟。这个过程有助于专注于准确测试&#34;该部分&#34;你实际上想要测试(而不是迷失为什么你的测试使用&#34;真正的其他代码&#34;给你带来麻烦)。
答案 1 :(得分:1)
恕我直言,我认为如果原始代码可以直接测试而不进行任何嘲弄会更好,因为这样可以减少错误,并避免争论如果模拟对象的行为与原始对象几乎相同,但我们不再生活在独角兽的世界里,嘲笑是必要的邪恶,还是不是?这仍然是个问题。
所以我想我可以将你的问题改为何时使用虚拟,假,存根或模拟? 通常,上述术语称为 Test doubles 。 首先,您可以查看此答案here
test doubles可能有用的一些案例:
正在测试的对象/正在测试的系统(SUT)许多依赖项(初始化目的所需)和这些依赖项不会影响测试,因此这些依赖项可能是虚拟的。
/**
* @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有一个外部依赖项,例如基础架构/资源(例如web service,数据库,现金,文件......),然后通过使用内存中表示来伪造它是一种很好的方法,如其中一个原因是为了避免使用测试数据来混淆此基础架构/资源。
/**
* @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的行为,那么模拟很可能会来到救援或存根(Test spy),可以检测到它很简单,因为很可能没有找到断言语句。例如,可以将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);