How To Properly Mock Yii::app() Components With Phpunit Getmock()

In our attempts to perform unit tests we have the following situation:

We have Controller that calls a component(c1). In this case the functions of c1 (In this case a component which calls Amazon AWS Cloud Search) are required to be mocked out in the case of the unit tests as I only care how the controller handles the response. I have looked thoroughly through the documentation and there is a setComponent function for the module, but not for the yii base. What I really want to do in this case is temporarily assign Yii::app()->c1 to be a PHPUnit Mock Class, so I can override the return values e.g.

Yii::app()->c1 = $this->getMock(‘SomeClass’, array(‘someFunction1’, ‘someFunction2’));

//Of course this is currently impossible in this syntax, I am simply demonstrating what I would LIKE to do

Through investigation I know I could create an actual Mock Component class and then in the test.php in the config directory, set it as ‘components’ => array(‘c1’=> array(‘class’=>‘mockClass’), however I do not want to have to override all the functions to return null (which PHPUnit mock already does) and write my own ->expects() method to handle different return values for different test cases.

I know I could also override the application using setApplication and feed in a mock application (as mentioned in thread /yiisoft/yii/issues/2389 on github.com, due to this being my first post I am unable to put the full url in.)to my unit tests. However in this example I only require a single specific mock component, and do not actually want to mock out my entire application.

I could of course not use Yii::app() in my controller and instead have components as instance variables of my class e.g

SomeClass {

   private $c2





   public function setC2($c2){


       $this->c2 = $c2


   }

}

Then call all my components inside the controller with $this->c2->function()

The issue we had with this was that programmers were putting the creation of these components inside the init() or __construct() function of our controller and components. The reasoning for this was to ensure those components were created when the Class was initialized so we could use them immediately. Due to this when they attempted to create the Component or Controller in a unit test, these init and construct methods were called and then the ACTUAL component was created. Although in this case were were able to override the component after, what this meant is that our unit tests were creating components that tried to connect with third party sources (something we do not want, we don’t want our unit tests to fail if for some reason a third party source is not reachable)

What I would prefer is to always call our "singleton" components using Yii::app()->someComponent->function() (in both unit tests and in our application), and then in the unit tests be able to override that someComponent at any time in my unit tests with a mock component.

One idea for your testing environment would be to :

  • create a test c1 component as a Mock Component

  • assign a private variable containing an instance of the actual c1 Component

  • override the __call() function to call the actual c1 Component as well

It works, but you wont be able to call private or protected methods. If you can afford it, declare them as public on the actual c1 Component.

Check: from Stack Overflow /questions/356128/can-i-extend-a-class-using-more-than-1-class-in-php

and from php.net/manual/ check keyword.extends.php#98665

Until mixins are properly supported properly that is the path to follow I guess.

Another idea would be to introduce Behaviors from your c1 component to your Mock Component.

Let us know if that helps, if you solve this successfully please get back to us.

P.S. Not being able to add links is highly annoying :)

I have found a fairly simple solution to this issue. The details of how to use this class are all in the comments of the code snippet below. Please feel free to share comments/suggestions/criticisms.


<?php

/**

 * TestCWebApplication class file.

 *

 * @author Liam McCarthy <liam.mccarthy@bluespurs.com>

 */


/**

 * TestCWebApplication is the base class override for all test cases

 * When one wants to call a component using Yii::app()->ComponentName->function() in their code but wants to be able to

 * mock out these components in their unit test, this will allow you to call overrideComponent() and then call this mock

 * object the same way without having to change your actual code for testing.

 *

 *

 * For example in your /protected/config/main.php

 *  'components' => array(

 *    'SomeClass'=> array(

 *      'class' => 'SomeClassFile',

 *      'instanceVariableName' => SOME_DEFINE_VALUE

 *    ),

 *  ),

 * When calling this component one would use in code Yii::app()->SomeClass->function()

 *

 * If one were to unit test a controller that had calls to this component using the Yii::app()-> method, and did not

 * want to use the actual class, but instead wanted to mock out the component, one could simply do as follows:

 *

 * in /protected/tests/bootstrap.php

 *

 * Change the line :

 *  Yii::createWebApplication($config);

 *

 * To the following lines (where PATH_TO_THE_FILE is the string representing the location of the file):

 *

 *  require_once PATH_TO_THE_FILE.'/TestCWebApplication.php';

 *  new TestCWebApplication($config);

 *

 * Then in the unit test you want to create add the following:

 *

 * public function setUp(){

 *   parent::setUp();

 *   Yii::app()->overrideComponent('SomeClass', $this->getMockBuilder('SomeClassFile')

 *    ->disableOriginalConstructor()

 *    ->setMethods(array('function', 'function1'))

 *    ->getMock());

 * }

 * public function tearDown(){

 *   parent::tearDown();

 *   Yii::app()->removeAllMocks();

 * }

 * Where function and function1 are two functions we want to mock out.

 * NOTE: There is also simpler syntax to mock out objects, this can be found in phpunit documentation.

 *

 * Once this is done, you will have a mock SomeClass object that will be called any time any function in your test calls

 * this function. You now will have to mock out the return values in each function that you require mocked returns.

 * e.g.

 *     Yii::app()->SomeClass->expects($this->any())

 *       ->method('function')

 *       ->will($this->returnValue("Hello World"));

 *

 * One could also mock out and remove a single mocked component within a single test, if the mocked component is removed

 * the next time it is called it will fall through to the Yii base functions and initialize a new component (REAL) to be

 * used.

 */


/**

 * @property array $mockComponents The array of all currently mocked out components, keyed on by the name

 *

 */

class TestCWebApplication extends CWebApplication{


  private $mockComponents;


  /* overrideComponent Creates an instance of an object, referenced by it's name, which will be callable through

   *                   Yii::app()

   * @author Liam McCarthy liam.mccarthy@bluespurs.com

   * @param string $className The class name of the object we are passing in, this is the name you will use to call

   *                          using Yii::app()->className->function()

   * @param mixed $classObject This is the object which we want to override the actual Yii::app()->Component with,

   *                           this will normally be a mock object generated by PHPUnit

   * @return null

   */

  public function overrideComponent($className, $classObject){

    $this->mockComponents[$className] = $classObject;

  }


/* removeMock Removes the mock object from the array $mockComponents

 * @author Liam McCarthy liam.mccarthy@bluespurs.com

 * @param string $className The class name of the object we are passing in, this is the name you will use to call

 *                          using Yii::app()->className->function()

 * @return null

 */

  public function removeMock($className){

    unset($this->mockComponents[$className]);

  }


  /* removeAllMocks Removes all the mock object from the array $mockComponents

 * @author Liam McCarthy liam.mccarthy@bluespurs.com

 * @return null

 */

  public function removeAllMocks(){

    foreach($this->mockComponents as $key => $value){

      unset($this->mockComponents[$key]);

    }

  }


/* __get Override of magic __get function, if we have the object in our current array, use it or default to Yii's magic

 *       __get function in the framework.

 * @author Liam McCarthy liam.mccarthy@bluespurs.com

 * @param string $name The name of the object we want to call in the example of Yii::app()->SomeClass,

 *                     $name = "SomeClass"

 * @return mixed The object we desire

 */

  public function __get($name)

  {

    if(isset($this->mockComponents[$name])){

      return $this->mockComponents[$name];

    }

    return parent::__get($name);

  }


}

1 Like
Unfortunately this did not solve the issue of preventing the original (REAL) component from being instantiated. For my current case if the init() function of the Component is ever called (In Yii usually on the first Yii::app()-&gt; call to the component), my unit tests would fail. I could not allow it to depend on an external source connectivity to test my functions. I also wanted to continue to call all my components within code using Yii::app()-&gt;Component-&gt;function() and only make mock calls for my unit tests. I do appreciate the reply however, thanks for your time&#33;
1 Like