64

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?

observer
  • 2,925
  • 1
  • 19
  • 38
rr.
  • 6,484
  • 9
  • 40
  • 48

8 Answers8

47

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.

k0pernikus
  • 60,309
  • 67
  • 216
  • 347
Gordon
  • 312,688
  • 75
  • 539
  • 559
  • 19
    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](http://misko.hevery.com/2008/12/15/static-methods-are-death-to-testability/)." – Brad Koch Oct 01 '12 at 15:30
  • 2
    The `staticExpects` function has been removed as of PHPUnit v4. See [this thread on github](https://github.com/sebastianbergmann/phpunit-mock-objects/issues/137) for an explanation why. – Arnold Daniels Nov 28 '15 at 00:51
  • 8
    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` ? – krishna Prasad Jan 17 '17 at 13:13
  • 1
    @krishnaPrasad see the linked github issue. there is none. – Gordon Jan 17 '17 at 13:17
  • @Gordon Thanks for the information, I had already checked and thought that might be some other way to achieve this, that's why asked here. Thanks for the confirmation. – krishna Prasad Jan 17 '17 at 13:26
  • 51
    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 – Sliq May 07 '20 at 15:53
  • 1
    This answer seems useless in 2022, despite having the most votes. I think it should be deleted, so that it doesn't get in the way of useful answers. – Ian Dunn May 05 '22 at 21:05
  • There is a shocking angle to this. Take a moment riddle me this. We have self driving cars, MidJourney AI platform, ChatGPT, Boston Dynamics robots, Reusable space rockets, we have at least a probe that is still functioning and has left the know solar system, cloning, gene editing, etc. and we are soon going to colonize Mars. Why is it that nobody can figure out how to mock static methods? Sigh We need to think outside the box and get rid of any conceptual blockages we might be subconsciously holding on to. We can do this. – asiby Feb 04 '23 at 05:24
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'); 
treeface
  • 13,270
  • 4
  • 51
  • 57
  • 11
    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 :) – pedromanoel Mar 04 '15 at 13:06
  • 1
    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! – Petr Urban Dec 15 '22 at 09:32
7

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);
    }
}
observer
  • 2,925
  • 1
  • 19
  • 38
b01
  • 4,076
  • 2
  • 30
  • 30
  • I use this method when I do not want to include an extra PHP library to do it for me. – b01 Nov 19 '17 at 13:49
4

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.

Ilia Yatsenko
  • 779
  • 4
  • 7
3

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

Ankit Raonka
  • 6,429
  • 10
  • 35
  • 54
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')));
Mikkel
  • 1,192
  • 9
  • 22
Leonid Shagabutdinov
  • 1,100
  • 10
  • 14
-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');
}
Serhii Vasko
  • 383
  • 4
  • 11
-2

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

Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • 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). – rr. Mar 01 '10 at 18:02
  • 30
    The question is definitely about mocking static methods -- telling the author to "not use static methods" doesn't cut the mustard. – Lotus Oct 02 '14 at 14:09