Search code examples
phplaravelphpunitmockery

Mocking static method in same Class (Mockery, Laravel9)


I am writing a unittest for a Laravel project.

I would like to test the public method testMethod() in the following class.

class Foo extends Model
{
    public static function staticMethod($arg)
    {
         return 'from staticMethod';
    }
    public function testMethod($arg)
    {
         $result = self::staticMethod($arg);
         return $result;
    }
}

Here is my test code:


Edit 2

Following @matiaslauriti 's advice, I rewrote the test code as follows

//I did not use 'overload:'. When I used it, I got the error 'testMethod() does not exist on this mock object' (even with makePartial).

$mock = \Mockery::mock(Foo::class)->makePartial();
$mock->shouldReceive('staticMethod')
    ->once()
    ->andReturn('mocked return value');

$this->app->instance(Foo::class, $mock);
 
$mockedFoo = app(Foo::class);

$result = $mockedFoo->testMethod('test argument');

Then I changed self:: to static:: as advised.

public function testMethod($arg)
    {
         return static::staticMethod($arg);
    }

When we did so, the test ran successfully and we were able to mock the static methods!!!!!


Edit 1

Following @Charlie's advice, I rewrote the test code as follows

 $mock = \Mockery::mock(Foo::class);
 $mock->shouldReceive('staticMethod')
    ->once()
    ->andReturn('mocked return value');

 $this->app->instance(Foo::class, $mock);
 
 $mockedFoo = app(Foo::class);

 $result = $mockedFoo->testMethod('test argument');

Here's one question.

// return 'mocked return value'
$mockedFoo->staticMethod('test');

// return value of original method.
Foo::staticMethod('test');

In the code under test, I am calling it with Foo::staticMethod(), not an instance.

I'm still trying this code, but is it possible to mock a statich method without overloading?

And this test returns;

Mockery\Exception\BadMethodCallException: Received Mockery_2_App_Models_Foo::testMethod(), but no expectations were specified

This error means that the mock recognizes testMethod() (I'm glad!).

But I want testMethod() to work according to the original code, so I don't want to set a return value for the mock.

So, I used makePartial().

$mock = \Mockery::mock('overload:'.Foo::class)->makePartial();

Then, from now on, the staticMethod() mock was not used when running the test, but the original staticMethod() was used...

Does anyone have any ideas?


Solution

  • You are nearly there, your issue with the new code is that you are not asking for partial mock, so whatever you have not mocked, will return an error (not defined). But you also need to still overload.

    I will share a solution, a quick fix for your code:

    $mock = \Mockery::mock('overload:'.Foo::class)->makePartial();
    $mock->shouldReceive('staticMethod')
        ->once()
        ->andReturn('mocked return value');
    
    $this->app->instance(Foo::class, $mock);
     
    $mockedFoo = app(Foo::class); // This part makes no sense on the test, as you already have the $mock, so directly call the mock
    
    $result = $mockedFoo->testMethod('test argument');
    

    But your original code needs to be changed, instead of self::staticMethod($arg), it must be static::staticMethod($arg) so when Mockery overloads the Foo class so it can simulate the static call, static refers to the late binding (Mockery is doing something like MockeryClass1 extends Foo, so if you have self, it will always refer to Foo, so no mock, but static will always refer to MockeryClass1 in this case, so the static mocked method will work).

    So, in code terms, Mockery does something like this:

    // When you do this
    $mock = \Mockery::mock('overload:'.Foo::class)->makePartial();
    
    // Mockery does something like
    class Mocker_class_fake_1 extends Foo
    {
        // ...
    
        public function __call()
        {
            // ...
        }
    
        public function __callStatic()
        {
            // ...
        }
    }
    
    /**
     * So when you do $mock->shouldReceive('xyz') and then $mock->xyz()
     * Mockery will use PHP __call ($this->method) magic method to resolve
     * what to do.
     *
     * __callStatic does the same but for static calls. (Class::method)
     * 
     * Having makePartial() does the same, only that will use original code
     * instead of erroring that there is no definition for that method call.
     */
    

    Now, if you code is:

    public function testMethod($arg)
    {
        return self::staticMethod($arg);
    }
    

    When using that Mocker_class_fake_1 example class, self resolves to Foo ALWAYS, but static resolves to the one extending and calling that method, in this case Mocker_class_fake_1::staticMethod($arg) and then __callStatic will be able to resolve staticMethod because you wrote shouldReceive('staticMethod'), etc.

    But you need the 3 things:

    • overload:.Foo::class
    • makePartial()
    • $this->app->instance(Foo::class, $mock); and app(Foo::class); (or resolve(Foo::class);, exact alias of app(...))

    My question would be why are you extending a Model and testing it, usually you do not mix this (of course you can, but that is a bad practice). So I would create a new domain class and separate the Model logic from the Domain logic, but I am not sure why you have it that way as you just put example code, not real code.

    Hope this clarifies more!

    More info about __call and __callStatic.