2

I had a discussion with my Team Lead, regarding UnitTest, the question was, In UnitTest do we use Object Mocking or use the Real Object? I was supporting the Object Mocking concept, as we should only input/output data from Objects.

At the end we agreed to use Real object instead of Mocking so the following was my Test

<?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, []]
        ];
    }
}

My question is, in Unit Test should is use Real Objects or Object Mocking and why? Second Question, the given Unit test is right or wrong in terms of Unit testing.

Dahab
  • 518
  • 1
  • 5
  • 23
  • IMHO you should mock every object you aren't testing so every change on the other classes don't reflect to the unit test. I.e. if you change the implementation of the PaymentMethodRuleManager or the PaymentMethodList class your test continue to work (why should break?) – Matteo Dec 07 '16 at 15:38

3 Answers3

1

The generic answer to such a generic question is: you prefer to use as much of "real" code as possible when doing unit tests. Real code should be default, mocked code is the exception!

But of course, there are various valid reasons to use mocking:

  • The "real" code does not work in your test setup.
  • You want to use your mocking framework also to verify that certain actions took place

Example: the code that you intend to test makes a call to some remote service (maybe a database server). Of course that means that you need some tests that do the end to end testing. But for many tests, it might be much more convenient to not do that remote call; instead you would use mocking here - to avoid the remote database call.

Alternatively, as suggested by John Joseph; you might also start with mocking all/most dependencies; to then gradually replace mocking with real calls. This process can help with staying focused on testing exactly "that part" that you actually want to test (instead of getting lost in figuring why your tests using "real other code" is giving you troubles).

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • +1. I'd also add that it's useful to _start_ with mock dependencies, and switch those out to the real objects when they are actually eventually created. This saves you from going off creating objects and losing focus on what you are testing at that time, and you can continue your subject test without the dependency blurring the design process. – John Joseph Dec 08 '16 at 11:23
  • @JohnJoseph That is an interesting aspect - thank you very much; I added that to my answer. – GhostCat Dec 08 '16 at 11:27
1

IMHO I think it would be good if the original code could be tested directly without any mocking as this would make it less error-prone, and would avoid the debate that if the mocked object behaves almost the same as the original one, but we are not living in the world of unicorns anymore, and mocking is a necessary evil or it is not? This remains the question.

So I think I can rephrase your question to be when to use dummy, fake, stub, or mock? Generally, the aforementioned terms are known as Test doubles. As a start, you can check this answer here

Some of the cases when test doubles might be good:

  • The object under test/System Under Test (SUT) a lot of dependencies, that are required for initialization purposes, and these dependencies would not affect the test, so these dependencies can be dummy ones.

    /**
     * @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 has an external dependencies such as an Infrastructure/Resource (e.g. web service, database, cash, file …), then it is a good approach to fake that by using in-memory representation, as one of the reasons to do that is to avoid cluttering this Infrastructure/Resource with test data.

    /**
     * @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];
                         }
                     )
               );
    }
    
  • When there is a need of asserting the state of SUT, then stubs become handy. Usually, this would be confused with a fake object, and to clear this out, fake objects are helping objects and they should never be asserted.

    /**
     * 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();
         }
    }
    
  • If the behavior of SUT should be checked then mocks most probably will come to the rescue or stubs (Test spy), it can be detected as simple as that most probably no assert statements should be found. for example, the mock can be setup to behave like when it get a call to X method with values a, and b return the value Y or expect a method to be called once or N of times, ..etc.

    /**
     * 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());
        }
    }
    

Corner cases

  • SUT has a lot of dependencies which are dependent on other things, and to avoid this dependency loop as we are only interested in testing some methods, the whole object can be mocked, but with having the ability to forward the calls to the original methods.

     $testDouble =  $this->getMockBuilder(Entity\Entity1::class)
                                ->disableOriginalConstructor()
                                ->setMethods(null);
    
Community
  • 1
  • 1
Ahmed Kamal
  • 126
  • 1
  • 2
  • 10
0

As per Ahmed Kamal's answer, it worked as expected.

I tested the below sample.

Foo.php

<?php

class Foo
{
    /**
     * Tell Foo class Name
     * @param string $name
     * @return string
     */
    public function tellName(string $name = 'Josh'): string
    {
        return 'Hi ' . $name;
    }
}

FooTest.php

<?php

include('Foo.php');

use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    /**
     * PHPUnit testing with assertEquals
     * @return void
     */
    public function testTellName()
    {
        // create the class object
        $mockObj = $this->getMockBuilder(Foo::class)
            ->disableOriginalConstructor()
            ->setMethods(null)
            ->getMock();
        // get the object function result by passing the method parameter value
        // pass different parameter value to get an invalid result
        $result = $mockObj->tellName('John');
        // validate the result with assertEquals()
        $this->assertEquals('Hi John', $result);
    }
}

Error and Success results:

enter image description here

enter image description here

Cheers!