68

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?

0

8 Answers 8

48

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.

Sign up to request clarification or add additional context in comments.

8 Comments

Worth noting "This approach only works for the stubbing and mocking of static method calls where caller and callee are in the same class. This is because static methods are death to testability."
The staticExpects function has been removed as of PHPUnit v4. See this thread on github for an explanation why.
As we know that staticExpects has been completed removed from recent version of PHPUnit, then what is the alternate way to achieve this without staticExpects ?
@krishnaPrasad see the linked github issue. there is none.
Good example of how useless "optimization" messes up things in the real world. staticExpects() was a well working tool that helped millions of people to write proper tests, but then some "smart" people decided to remove it, knowing that there's no alternative and people will simply have to remove tests. Even now, 7+ years later, major Frameworks and libraries rely on lots of static calls, which are now not testable. To sum this up: We WANT to write tests and mock external libs, but cannot, because the developers of PHPUnit decided to remove it. The result: No tests at all. Thanks
|
15

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'); 

2 Comments

This library is gold! But I think they should put a disclaimer on their page: "Just because you can test global functions and static methods with our library, this doesn't mean you should write new code this way." I read somewhere that a bad test is better than not having tests at all, and with this library you can add a safety net to your legacy code. Just make sure to write new code in a better way :)
Mocks of static functions / methods using AspectMock are persistent across tests. Once you mock a static method in test one and call it in next test, it will use mock of the previous test. Within the same class or even in another test class completely separate from the first test class. This makes it very hard to utilise in real world as your tests might work in separation but start failing when you run multiple tests (ie. test suite). There is a workaround to use @runInSeparateProcess annotation for tests where AspectMock is used to mock static functions - that is very far from ideal!
9

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);
    }
}

1 Comment

I use this method when I do not want to include an extra PHP library to do it for me.
6

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.

3 Comments

The age of the topic isn't relevant, you should always post an answer on old topics if you have something helpful to add.
Works to mock a whole class, but how do you mock a single static method and use the real implementation for the rest?
With this approach it is not possible, I think.
4

Mockery's Alias functionality can be used to mock public static methods

http://docs.mockery.io/en/latest/reference/creating_test_doubles.html#creating-test-doubles-aliasing

Comments

0

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')));

1 Comment

Doesn't has a current stable version :\
-1

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');
}

1 Comment

This seems like a duplicate of stackoverflow.com/a/47377592/450127
-3

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...

2 Comments

Thanks for the suggestion, it's more style than anything. I could make the method non static in this particular instance (though I'd prefer being able to use it without instantiating).
The question is definitely about mocking static methods -- telling the author to "not use static methods" doesn't cut the mustard.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.