Php 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\

我和我的团队负责人讨论了关于UnitTest的问题, 在UnitTest中,我们是使用对象模拟还是使用真实对象? 我支持对象模拟的概念,因为我们应该只从对象输入/输出数据

最后,我们同意使用真实对象而不是模拟,所以下面是我的测试

<?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我认为如果可以直接测试原始代码而不进行任何模拟会更好,因为这样会减少出错的可能性,我们可以避免这样的争论:如果被模仿的对象的行为与原始对象几乎相同,但我们不再生活在独角兽的世界里,那么模仿是一种必要的邪恶,还是不是?这仍然是个问题

因此,我想我可以将您的问题重新表述为何时使用dummyfakestubmock? 通常,上述术语称为。 首先,您可以检查这个答案

有些情况下可能是好的:

  • 被测对象/被测系统()有很多依赖项,这些依赖项是初始化所必需的,并且这些依赖项不会影响测试,因此这些依赖项可以是虚拟的

    /**
     * @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这是一个有趣的方面-非常感谢;我在回答中加了这个。