Summer Sale! All accounts are 50% off this week.

Smiffy's avatar

Mocking model from within Testbench and Controller

I'm using testbench for a package and have provided a controller. What I want to do is ensure that a partiicualr method is called on an eloquent model:

class MyController extends Controller
{
	public function __invoke()
	{
		if($test_model_id = request()->get('test_model_id') {
			$test_model = TestModel::where('id', $test_model_id)->first();
			$test_model->do_something();
			...
		} else {
			abort(404);
	}
}

I can do a basic test this with:

$this->get('/my_route?test_order_id=1')->assertSuccessful();

But I would like to test that the method do_something is called on the model.

I was hoping to mock the request object but unsure how to do this within TestBench and when calling a route on a Controller. My original approach was:

    $mock = $this->mock(TestModel::class, function (\Mockery\MockInterface $mock) {
        $mock->shouldReceive('do_something')->once();
    });

    $model = TestModel::factory()->create();

	$this->get('/my_route?test_order_id='.$model->id)->assertSuccessful();

However this results in Mockery stating that the Method do_something(<Any Arguments>) ... should be called exactly 1 times but called 0 times. I'm guessing because it is actually returning a "real" TestModel within the controller? Is there a way to return a mock from TestModel::first() or perhaps a different approach entirely?

0 likes
5 replies
tisuchi's avatar

@smiffy How about this?

$mock = $this->mock(TestModel::class, function (\Mockery\MockInterface $mock) {
    $mock->shouldReceive('do_something')->once();
});

$model = TestModel::factory()->create();

$response = $this->get('/my_route?test_order_id='.$model->id);
$response->assertSuccessful();

$controller = $response->instance();
$controller->test_model = $mock;

$mock->shouldHaveReceived('do_something');
Smiffy's avatar

@tisuchi Thank you. However, that method doesn't seem to exist:

BadMethodCallException : Method Illuminate\Http\Response::instance does not exist.
tisuchi's avatar

@Smiffy hmm... How about you try this way?

$controller = app()->make(MyController::class);
$controller->test_model = $mock;
Smiffy's avatar

@tisuchi $test_model isn't a property on the controller so won't be able to access it. I've managed to find a way though! :)

Smiffy's avatar

Decided to rewrite my controller and use the Service Container so that I mock the object and replace it when testing. In my service container:

$this->app->when(MyController::class)
    ->needs('$test_model')
    ->give(function() {
        if($test_model_id = request()->get('test_model_id')) {
            return TestModel::where('id', $test_model_id)->first();
        }
    });

Then rejig the controller to accept TestModel as an argument which will be injected by the Service Container:

class MyController extends Controller
{
	public function __invoke(TestModel $test_model)
	{
		if($test_model) {
			$test_model = TestModel::where('id', $test_model_id)->first();
			$test_model->do_something();
			...
		} else {
			abort(404);
	}
}

My test can just do the follwing:

$this->instance(TestModel::class, $this->partialMock(TestModel::class, function (\Mockery\MockInterface $mock) {
            $mock->shouldReceive('do_something')->once();
        }));

$this->get('/my_route?test_order_id='.$model->id)->assertSuccessful();

This now passes.

One thing to bear in mind is that, it doesn't matter what my get params are for the test - the mock ensures that a mocked object will always be passed to the controller. So I'll probably go onto to write a test passing null to test failure and another to ensure my binding in the Service Container is correct.

Please or to participate in this conversation.