Laravel: how to mock dependency injection class methods

て烟熏妆下的殇ゞ 提交于 2020-02-24 10:17:30

问题


I'm using the GitHub API through a Laravel API Wrapper. I've created a dependency injection class. How can I mock the exists method within the App\Http\GitHub.php class?

App\Http\GitHub.php:

use GrahamCampbell\GitHub\GitHubManager;

class Github
{
    public $username;

    public $repository;

    public function __construct($username, $repository, GitHubManager $github)
    {
        $this->username = $username;

        $this->repository = $repository;

        $this->github = $github;
    }

    public static function make($username, $repository)
    {
        return new static($username, $repository, app(GitHubManager::class));
    }

    /**
     * Checks that a given path exists in a repository.
     *
     * @param  string  $path
     * @return bool
     */
    public function exists($path)
    {
        return $this->github->repository()->contents()->exists($this->username, $this->repository, $path);
    }
}

Test:

    use App\Http\GitHub;
    public function test_it_can_check_if_github_file_exists()
    {
        $m = Mockery::mock(GitHub::class);
        $m->shouldReceive('exists')->andReturn(true);
        app()->instance(GitHub::class, $m);

        $github = GitHub::make('foo', 'bar');

        $this->assertTrue($github->exists('composer.lock'));
    }

Running this test actually hits the API rather than just returning the mocked true value, what am I doing wrong here?


回答1:


There is tree problems here, the way you instantiate your object. The way you are calling two methods on your mock object and you are binding it to the wrong instance.

Dependency injection

Static methods in general is an anti pattern and constructor parameters does not work with how the container works, therefor you would not be able to use resolve(Github::class);. Usually Laravel classes solve this by using setters.

class Github
{
    public $username;

    public $repository;

    public $github;

    public function __construct(GitHubManager $github)
    {
        $this->github = $github;
    }

    public function setUsername(string $username) {
        $this->username = $username;

        return $this;
    }

    public function setRepository(string $repository) {
        $this->repository = $repository;

        return $this;
    }
}

Now you can call your code with the following approach.

resolve(Github::class)->setUsername('Martin')->setRepository('my-repo')->exists();

The chaining of methods

Here there are two calls to the mock object, they are chaining, so you should create a mock chain similar to this. Right now the mock object would not know contents and therefor fail.

$m = Mockery::mock(GitHub::class);

$m->shouldReceive('contents')
    ->andReturn($m);

$m->shouldReceive('exists')
    ->with('Martin', 'my-repo', 'your-path')
    ->once()
    ->andReturn(true);

Binding the instance

Working with the container, it will automatically load it based on the classes, so the following code will dependency inject GithubManager if resolved with app(), resolve() or in a constructor.

public function __construct(GithubManager $manager)

This code will inject GithubManager in my resolve example above, but in your example you are binding it to the GitHub class, which wont automatically load and you should always mock the class farthest down the chain. Therefor you instance bind should be.

app()->instance(GitHubManager::class, $m);



回答2:


Your problem is that when you initialise your github object you are not referencing the object in the Service Container.

        // Initialises an object in the service container.
        app()->instance(GitHub::class, $m);

        // Creates a new object from the class and doesn't use the one in the container.
        $github = GitHub::make('foo', 'bar');

The Service Container is essentially a box with all your initialised objects in it and you can reference them at any time during the Laravel lifecycle. This pattern allows us to do things such as Dependency Injection in a clean manner and as a result we can test when classes are called because we can "swap" out what's in the box with whatever we want.

Laravel has abstracted all of the above away for us by using the mocking functions. Personally I just use spy for everything so I don't have to remember what the others do (granted there are situation where you need to use the others).

Now for the solution:

    public function test_it_can_check_if_github_file_exists()
    {
        // Initialise GitHub::class into the service container
        $gitHubSpy = $this->spy(GitHub::class);

        // Mock the function
        $gitHubSpy->shouldReceive('exists')
            ->andReturn(true);

        // Assert we have mocked correctly
        $this->assertTrue($gitHubSpy->exists('composer.lock'));
    }

In a real world situation you would most likely want to assert that your production code called a function which you can do by doing:

$gitHubSpy->shouldReceive('exists')->with('composer.lock')->andReturn(true);


来源:https://stackoverflow.com/questions/60342730/laravel-how-to-mock-dependency-injection-class-methods

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!