NMR's avatar
Level 4

TDD Dependency binder not working on tests

Hi,

I'm having a hard time with TDD. This is the first time i'm trying to work the Laravel & TDD way on my projects. I really want to do this right, the thing is that i already built some of the stuff and i wanted to create some unit tests, which means to mock dependencies "on the fly" for your interfaces/contracts.

I Find myself creating mocks with Mockery and then using $this->app->instance('abstract', $mockFoo');

So then I can inject them and replace the FooServiceProvider bindings for that specific object. In order to test a class with dependencies.

The thing is that it isn't working at all... It just uses the actual classes and the only way is to do something like this (pseudo code):

$bar = new BarController($mockFoo);

$thingy = $bar->thingy();

$this->assertThingy($thingy);

I want to do

$this->app->instance('abstract', $mockFoo');

$this->get('/thingy/1');

$this->assertStatusOk();

$this->assertMorethings(); //...

This is the actual code:

/** @test */
public function can_be_called_with_country_ARG_or_ESP_and_valid_customer_number_from_9_to_12_digits()
{
    // Dependencies:
    $actualCustomerNumber = "000086866920";
    $domApiMock = Mockery::mock(\App\Dom\Contracts\DomApiRequest::class);
    
    /** @var \App\Dom\Contracts\DomApiRequest $domApiMock | proxy mocked object */
    $this->app->instance(\App\Dom\Contracts\DomApiRequest::class, $domApiMock);
    $domApiMock
        ->shouldReceive('get')
        ->with('', ['country' => 'ARG', 'number' => $this->fakeCustomer], [])
        ->once()->andReturn([]);
    

    // Execute
    $this->get('country/ARG/customer/' . $actualCustomerNumber, $this->headers);

    //Check
    $this->seeJson([]);
    $this->assertResponseStatus(204);
}
0 likes
6 replies
bobbybouwmann's avatar

You need to set the instance after you have added the shouldReceive. The current $domApiMock doesn't know about the shouldReceive when you set it as the instance in the container.

As far as I can see flipping these two calls should fix your problem. If not, let me know ;)

NMR's avatar
Level 4

Thank you for answering @bobbybouwmann . Unfortunately this has not been the solution. Let me show you the small implementation that's currently working (which i dded to debug a little bit).

public function get(string $route, array $params, array $middlewares): JsonResponse
{
    dd('', 'we are at implementation, not the mock.', __CLASS__ . '::'. __FUNCTION__ . ':'.  __LINE__);
    try {
        return $this->request(__FUNCTION__, $route, $params, $middlewares);
    } catch (\Exception $exception) {
        return response()->json($this->exceptionResponseMessage, 502);
    }
}

Test is looking like you suggested (or i think it is):

/** @test */
public function can_be_called_with_country_ARG_or_ESP_and_valid_customer_number_from_9_to_12_digits()
{
    // Dependencies:
    $actualCustomerNumber = "000086866920";
    $domApiMock = Mockery::mock(\App\Dom\Contracts\DomApiRequest::class);

    /** @var \App\Dom\Contracts\DomApiRequest $domApiMock | proxy mocked object */
    $domApiMock
        ->shouldReceive('get')
        ->with('', ['country' => 'ARG', 'number' => $this->fakeCustomer], [])
        ->once()->andReturn([]);

    $this->app->instance(\App\Dom\Contracts\DomApiRequest::class, $domApiMock);


    // Execute
    $this->get('country/ARG/customer/' . $actualCustomerNumber, $this->headers);

    //Check
    $this->seeJson([]);
    $this->assertResponseStatus(204);
}

When running phpunit i get:

PHPUnit Pretty Result Printer 0.20.3 by Codedungeon and contributors. ==> Configuration: ~/PhpstormProjects/ig-internal-api/phpunit-printer.yml

PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

==> BaseRouteTest ✓ "" "we are at implementation, not the mock." "App\Dom\AdvantageApi::get:29"

bobbybouwmann's avatar

Are you sure the get is getting all the provided parameters like you have in your test? You can also use withAnyArgs so the whole method call is mocked!

$domApiMock = Mockery::mock(\App\Dom\Contracts\DomApiRequest::class);
$domApiMock
    ->shouldReceive('get')
        ->withAnyArgs()
    ->once()
    ->andReturn([]);

$this->app->instance(\App\Dom\Contracts\DomApiRequest::class, $domApiMock);
NMR's avatar
Level 4

@bobbybouwmann this is the contract i'm supposed to use:

function get(string $route, array $params, array $middlewares): JsonResponse;

function post(string $route, array $params, array $middlewares): JsonResponse;

I believe the problem lays in the $this->app->instance(...) method, which is not binding a current object to the Container, but that's just a guess, and i'm not able to prove that.

This is the ServiceProvider Dependency binder for this contract (GetCustomer is a Controller):

public function register()
{
    $this->app->when(GetCustomer::class)
        ->needs(DomApiRequest::class)
        ->give(AdvantageApi::class);
}

I've run the test again changing ->withArgs(...) for ->withManyArgs() and the result is the same.

:(

bobbybouwmann's avatar

It's working fine for me, I'm using the same approach in many of my projects and tests!

Also I'm not sure, but should the method give return some value? Right now you return the class name itself, but not the instance of the class. At least that's what's working for me most of the time!

public function register()
{
    $this->app->when(GetCustomer::class)
        ->needs(DomApiRequest::class)
        ->give(function () {
            return new AdvantageApi()
        });
}
NMR's avatar
Level 4

As far as i'm aware, you can use both, class string or Closure, if you need to resolve some dependencies from a config when you're building the main dependency.

/**
 * Define the implementation for the contextual binding.
 *
 * @param  \Closure|string  $implementation
 * @return void
 */
public function give($implementation);

The class AdvantageApi also has dependencies (on config data) so i deferred the building of it to the container. Using this aproach, the issue persists.

public function register()
{
    $this->app->when(GetCustomer::class)
        ->needs(DomApiRequest::class)
        ->give(function () {
            return $this->app->make(AdvantageApi::class);
        });
}

This is in the ApiServiceProvider::register

    $this->app->bind(AdvantageApi::class, function (Application $app) {
        return new AdvantageApi(
            config('dom.http-client.advantage'),
            $app->make(ApiResourceManager::class)
        );
    });

Maybe there is something to tinker with on the phpunit.xml or something else. Dunno, i don't know what's going on and i can't debug it or at least understand where the problem might be. Maybe in full Laravel this works but here in Lumen i've to enable some setting or do something at startUp method. Documentation doesn't have much information regarding this, neither forums or stackOverflow.

Please or to participate in this conversation.