简体   繁体   中英

PHPUnit Mock Objects and Static Methods

I am looking for the best way to go about testing the following static method (specifically using a Doctrine Model):

class Model_User extends Doctrine_Record
{
    public static function create($userData)
    {
        $newUser = new self();
        $newUser->fromArray($userData);
        $newUser->save();
    }
}

Ideally, I would use a mock object to ensure that fromArray (with the supplied user data) and save were called, but that's not possible as the method is static.

Any suggestions?

Sebastian Bergmann, the author of PHPUnit, recently had a blog post about Stubbing and Mocking Static Methods . With PHPUnit 3.5 and PHP 5.3 as well as consistent use of late static binding, you can do

$class::staticExpects($this->any())
      ->method('helper')
      ->will($this->returnValue('bar'));

Update: staticExpects is deprecated as of PHPUnit 3.8 and will be removed completely with later versions.

There is now the AspectMock library to help with this:

https://github.com/Codeception/AspectMock

$this->assertEquals('users', UserModel::tableName());   
$userModel = test::double('UserModel', ['tableName' => 'my_users']);
$this->assertEquals('my_users', UserModel::tableName());
$userModel->verifyInvoked('tableName'); 

I would make a new class in the unit test namespace that extends the Model_User and test that. Here's an example:

Original class:

class Model_User extends Doctrine_Record
{
    public static function create($userData)
    {
        $newUser = new self();
        $newUser->fromArray($userData);
        $newUser->save();
    }
}

Mock Class to call in unit test(s):

use \Model_User
class Mock_Model_User extends Model_User
{
    /** \PHPUnit\Framework\TestCase */
    public static $test;

    // This class inherits all the original classes functions.
    // However, you can override the methods and use the $test property
    // to perform some assertions.
}

In your unit test:

use Module_User;
use PHPUnit\Framework\TestCase;

class Model_UserTest extends TestCase
{
    function testCanInitialize()
    {   
        $userDataFixture = []; // Made an assumption user data would be an array.
        $sut = new Mock_Model_User::create($userDataFixture); // calls the parent ::create method, so the real thing.

        $sut::test = $this; // This is just here to show possibilities.

        $this->assertInstanceOf(Model_User::class, $sut);
    }
}

The doublit library could also help you to test static methods :

/* Create a mock instance of your class */
$double = Doublit::mock_instance(Model_User::class);

/* Test the "create" method */
$double::_method('create')
   ->count(1) // test that the method is called once
   ->args([Constraints::isInstanceOf('array')]) // test that first argument is an array
   ->stub('my_value') // stub the method to return "myvalue"

Another possible approach is with the Moka library:

$modelClass = Moka::mockClass('Model_User', [ 
    'fromArray' => null, 
    'save' => null
]);

$modelClass::create('DATA');
$this->assertEquals(['DATA'], $modelClass::$moka->report('fromArray')[0]);
$this->assertEquals(1, sizeof($modelClass::$moka->report('save')));

One more approach :

class Experiment
{
    public static function getVariant($userId, $experimentName) 
    {
        $experiment = self::loadExperimentJson($experimentName):
        return $userId % 10 > 5;  // some sort of bucketing
    } 

    protected static function loadExperimentJson($experimentName)
    {
        // ... do something
    }
}

In my ExperimentTest.php

class ExperimentTest extends \Experiment
{
    public static function loadExperimentJson($experimentName) 
    {
        return "{
            "name": "TestExperiment",
            "variants": ["a", "b"],
            ... etc
        }"
    }
}

And then I would use it like so:

public function test_Experiment_getVariantForExperiment()
{
    $variant = ExperimentTest::getVariant(123, 'blah');
    $this->assertEquals($variant, 'a');

    $variant = ExperimentTest::getVariant(124, 'blah');
    $this->assertEquals($variant, 'b');
}

Found the working solution, would to share it despite the topic is old. class_alias can substitute classes which are not autoloaded yet (works only if you use autoloading, not include/require files directly). For example, our code:

class MyClass
{
   public function someAction() {
      StaticHelper::staticAction();
   }
}

Our test:

class MyClassTest 
{
   public function __construct() {
      // works only if StaticHelper is not autoloaded yet!
      class_alias(StaticHelperMock::class, StaticHelper::class);
   }

   public function test_some_action() {
      $sut = new MyClass();
      $myClass->someAction();
   }
}

Our mock:

class StaticHelperMock
{
   public static function staticAction() {
      // here implement the mock logic, e.g return some pre-defined value, etc 
   }
}

This simple solution doesn't need any special libs or extensions.

Testing static methods is generally considered as a bit hard (as you probably already noticed) , especially before PHP 5.3.

Could you not modify your code to not use static a method ? I don't really see why you're using a static method here, in fact ; this could probably be re-written to some non-static code, could it not ?


For instance, could something like this not do the trick :

class Model_User extends Doctrine_Record
{
    public function saveFromArray($userData)
    {
        $this->fromArray($userData);
        $this->save();
    }
}

Not sure what you'll be testing ; but, at least, no static method anymore...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM