UnitTest对象模拟或真实对象

时间:2016-12-07 14:52:08

标签: php unit-testing mocking phpunit

我与我的团队负责人就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, []]
        ];
    }
}

我的问题是,在单元测试中应该使用真实对象还是对象模拟?为什么? 第二个问题,给定的单元测试在单元测试方面是对还是错。

2 个答案:

答案 0 :(得分:2)

这个一般性问题的一般答案是:你更喜欢使用&#34;真实的&#34;进行单元测试时尽可能编码。 Real 代码应为默认模拟代码为例外

但是,当然,使用模拟

有各种正当理由
  • &#34;真实&#34;代码在您的测试设置中不起作用。
  • 您还希望使用您的模拟框架验证某些操作已发生

示例:您要测试的代码会调用某些远程服务(可能是数据库服务器)。当然,这意味着您需要一些进行端到端测试的测试。但是对于许多测试来说,进行远程调用可能会更方便;相反,你会在这里使用模拟 - 避免远程数据库调用。

或者,正如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);