framework: Can't mock an object resolved with some constructor arguments

  • Laravel Version: 8.44.0
  • PHP Version: 8.0.2
  • Database Driver & Version: MySQL 8.0

Description:

When resolving an object with constructor parameters using the service container, it seems that subsequently trying to mock that object fails.

Steps To Reproduce:

Say I’ve got an interface FooContract, with a bar method:

<?php

namespace App;

interface FooContract
{
    public function bar(): string;
}

And a class Foo implementing that interface, with a constructor expecting a $baz string, and the bar method returning ‘qux’:

<?php

namespace App;

class Foo implements FooContract
{
    public function __construct(public string $baz)
    {
    }

    public function bar(): string
    {
        return 'qux';
    }
}

And I bind FooContract to Foo in AppServiceProvider:

public function register()
{
    $this->app->bind(FooContract::class, Foo::class);
}

Say I write a simple test.

Using the service container to resolve FooContract works as expected:

public function testFoo()
{
    resolve(FooContract::class, ['baz' => 'baz'])->bar(); // "qux"
}

But then if I create a mock and bind it to FooContract, the mock seems to be ignored:

public function testFoo()
{
    $this->mock(FooContract::class, function (MockInterface $mock) {
        $mock->shouldReceive('bar')->once()->andReturn('corge');
    });

    resolve(FooContract::class, ['baz' => 'baz'])->bar(); // still "qux"
}

Without changing the test, and simply by removing the arguments when resolving FooContract using the service container, the mock is not ignored anymore:

public function testFoo()
{
    $this->mock(FooContract::class, function (MockInterface $mock) {
        $mock->shouldReceive('bar')->once()->andReturn('corge');
    });

    //resolve(FooContract::class, ['baz' => 'baz'])->bar();
    resolve(FooContract::class)->bar(); // "corge"
}

I would expect the bind to work whether there are constructor parameters or not.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 23 (15 by maintainers)

Most upvoted comments

Still have this issue on Laravel 9.14.1. @osteel Found any workarounds?

Edit: I found a solution here. But, it would be nice if $this->mock could be used.

replicating the issue in the service provider (no mock)

In the non-working version - a concrete FooContract is registered as an instance in the container. This action does not replace the original binding.

The documentation does state that:

The given instance will always be returned on subsequent calls into the container

but instances are not registered as actual singletons. A new instance will be returned if it’s explicitly requested (eg telling it to use new constructor arguments).

working version

Replacing the call to instance with an explicit singleton binding here should fix this.

If we do expect a mocked instance to be bound as a singleton then we simply need to register it as one.

Container.php:740

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
    return $this->instances[$abstract];
}

My 2 cents here, it is determined as $needsContextualBuild so it skips returning the registered instance (which is the mock) and builds it again, thus receiving the original concrete instead of the mocked one. I’m not sure what the purpose of the contextual build is so can’t really tell what the correct way to fix this should be.