rfountain's avatar

How to Mock a dependency injection in Controller Test isolation

I'm trying to test my controllers in isolation by mocking one of my eloquent models. I can mock the model in my test

$this->mock = Mockery::mock(Server::class);
$this->app->instance(Server::class, $this->mock);

$this->mock->shouldReceive('delete')->once()->andReturn('deleted');

$this->call('delete', 'server/1');

The problem I'm having is my controller injects the Server indsance through the method and that's causing my tests not to work.

   /**
     * Remove the specified resource from storage.
     *
     * @param Server $server
     *
     * @return void
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function destroy(Server $server)
    {
        $this->repository->delete($server->id);
    }

The error I'm receiving is Mockery\Exception\BadMethodCallException : Received Mockery_0_App_Server::getAttribute(), but no expectations were specified

Is there a way to mock the dependency injection that Laravel does automatically?

0 likes
7 replies
Talinon's avatar

@rfountain

Laravel should mock the injection. It looks like you might need to mock your repository's method?

You're calling delete() on your repository, not the mocked Server object.

Does your repository instantiate a Server object and call delete on it?

rfountain's avatar

I was thinking that as well hit when I try to mock the repository my tests scream that it cannot find the servers table. The only place I use the servers model is on the repository which is why its confusing

rfountain's avatar

@talinon

here's my code for the repository

<?php


namespace App\Repositories;


use App\Server;
use Illuminate\Support\Collection;

/**
 * Class ServerRepository
 * @package App\Repositories
 */
class ServerRepository implements RepositoryInterface
{

    /**
     * @var Server
     */
    protected $model;

    /**
     * ServerRepository constructor.
     *
     * @param Server $server
     */
    public function __construct(Server $server)
    {
        $this->model = $server;
    }

    /**
     * @param $id
     *
     * @return mixed
     */
    public function get($id)
    {
        return $this->model->find($id);
    }

    /**
     * @return mixed
     */
    public function all()
    {
       return $this->model->all();
    }

    /**
     * @return Collection
     */
    public function getForUser()
    {
        return $this->model->getForUser();
    }

    public function getCompletedForUser()
    {
        return $this->model->getCompletedForUser();
    }

    /**
     * @param $id
     *
     * @return mixed
     */
    public function delete($id)
    {
        return $this->model->find($id)->delete();
    }

    /**
     * @param $id
     * @param $attributes
     *
     * @return mixed
     */
    public function update($id, $attributes)
    {
       return $this->get($id)->update($attributes);
    }

    /**
     * @param $attributes
     *
     * @return mixed
     */
    public function create($attributes)
    {
        return $this->model->create($attributes);
    }
}
Talinon's avatar

@rfountain

I'm pretty sure this is what's happening:

public function delete($id)
    {
        return $this->model->find($id)->delete();
    }

You're calling find() before delete(), which is throwing the expected exception.

There are a few things you need to review:

You have a constructor for your repository that expects a model of Server - so how are you instantiating $repository within your controller without knowing the model?

You are route-model binding your Server model within your controller's destroy() method, which retrieves the model. You then pass the id into your repository, where you look up the model again by its id, then delete it.

If you want a pure repository, remove the type-hint from your controller and pass in the id of the Server model. Then let your repository be responsible for retrieving it by its id and then deleting it.

 public function destroy($serverId)
    {
        $this->repository->delete($serverId);
    }

But even then, within your respository, this doesn't make sense:

     return $this->model->find($id)->delete(); 

You're calling find() on a model that you already injected into the constructor (from somewhere?)

Maybe just do this instead:

 public function delete($id)
    {
        return Server::findOrFail($id)->delete();
    }

Then within your test, set up an expectation for both find() and delete() to both be called once.

rfountain's avatar

@talinon Thanks so much for your detailed reply. Your suggestions are spot on. I made the changes you suggested but unfortunately, I'm still getting an error.

here's my test

 /** @test * */
    public function it_deletes_a_server()
    {
        $this->mock->shouldReceive('delete')->once()->andReturn(true);
        $response = $this->call('delete', 'servers/1')->assertStatus(200);
    }

the delete method from the repository

  /**
     * @param $id
     *
     * @return mixed
     */
    public function delete($id)
    {
        Server::findOrFail($id)->delete();
    }

The error I'm getting with exception handling off is

Illuminate\Database\QueryException : SQLSTATE[HY000]: General error: 1 no such table: servers (SQL: select * from "servers" where "servers"."id" = 1 and "servers"."deleted_at" is null limit 1)

If I'm mocking the repository, how would the Server query get called?

Talinon's avatar

@rfountain

You're not mocking your repository. You created a mock of your Server model. So, when you call Server::findOrFail()... within your repository, you're getting a new instance of Server, not your mock.

But - why do you even want to mock your repository? If you're testing if a server can be deleted, don't you want to actually assert that the deletion was made? By mocking, what are you actually testing here? According to your test, all you're really testing is that you get a 200 response, and that your repository returns true - but again, does that assert that it was actually deleted within the database?

Forgive me for making an assumption - but it looks like you just learned about the repository pattern and are trying to force it into a situation where it likely just isn't necessary. We've all been there - I remember trying to do the same thing. Have you considered just embracing how easy eloquent makes this without the over-complications of a repository? Do you ever foresee deleting a "Server" that isn't an Eloquent model? These are the questions you need to ask yourself.

Anyway.. just for learning purposes, if you wanted to make this test work, I think you'd need to bind your repository into the container, or bind an interface (which is often the case with repositories), so that you can swap it out with a mock when testing. Something like:

 /** @test * */
    public function it_deletes_a_server()
    {
    $this->mock = Mockery::mock(ServerRepository::class);
    $this->app->instance(ServerRepositoryInterface::class, $this->mock);

    $this->mock->shouldReceive('delete')->once()->andReturn('deleted');

    $this->call('delete', 'server/1');
    }

Then within your controller:

   public function destroy($serverId, ServerRepositoryInterface $repository)
    {
        $repository->delete($serverId);
    }

Laravel will automatically resolve your ServerRepository from the type-hint, as long as you have it bound into the container. Something like this within your AppServiceProvider:

$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);

However, you can see how complicated this gets where something as simple as below would likely do:

   public function destroy(Server $server)
    {
        $server->delete();
    }

Then your test could be:

/** @test * */
    public function a_server_may_be_deleted()
    {

    $server = factory(Server::class)->create();

        $response = $this->call('delete', "servers/{$server->id}")->assertStatus(200);

    $this->assertDatabaseMissing('servers', ['id' => $server->id]);
    }

Just giving you some things to think over and consider.

rfountain's avatar

@talinon thank you again for taking the time to reply. I'm actually trying to learn Mockery and testing in isolation rather than the repository pattern and it's proving more difficult than expected. To answer some of your questions

My setUp method in my test class does the mocking and the instance binding as well and I have bound my RepositoryInterface to the ServerRepository in the AppServiceProvider. It seems like everytime I try to mock the repository the server eloquent queries are being executed and I'm trying not to touch my database within this specific series of tests

You are definitely correct, I am overcomplicating things. I think that's because I'm trying to learn too many new things at once. I think what I'm going to do is simplify things as you state and maybe get rid of the repository because you're correct, I'll never use anything other than eloquent. I was just trying to learn how to decouple my controllers from the model.

Thanks again for all your help

Please or to participate in this conversation.