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)
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:
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 explicitsingleton
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
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.