skip to Main Content

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:

1.

$mock = Mockery::mock('overload:'.Foo::class);
$mock->shouldReceive('staticMethod')
    ->once()
    ->with('test argument')
    ->andReturn('mocked return value');

$foo = new Foo();
$result = $foo->testMethod('test argument');

The above returns the result that: testMethod() does not exist on this mock object.

2.

$mock = Mockery::mock(Foo::class);

$mock->shouldReceive('staticMethod')
    ->once()
    ->with('test argument')
    ->andReturn('mocked return value');

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

$foo = new Foo();
$result = $foo->testMethod('test argument');

In this case, however, staticMethod() was not mocked.


Edit:

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;

MockeryExceptionBadMethodCallException: 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?

2

Answers


  1. Two of your test contains same issue, you didn’t use Laravel app to get the instantiated class, when you use new Foo(), it actually instantiated a real Foo::class instead of the mocked class.

    An untested example:

    $mock = Mockery::mock(Foo::class);
    
    $mock->shouldReceive('staticMethod')->once()->with('test argument')->andReturn('mocked return value');
    
    // This line will replace the origin Foo::class with the mocked Foo::class.
    $this->app->instance(Foo::class, $mock);
    
    // $foo = new Foo(); Instantiate like this won't use the mocked Foo::class.
    
    // Use "app(Foo::class)" to instantiate the mocked class.
    $mockedFoo = $this->app(Foo::class);
    
    $result = $mockedFoo->testMethod('test argument');`
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search