Rooduef's avatar

Laravel 5.8 interface binding while running tests

Hi everyone.

I'm stuck with the next problem.

I writing an external API client. It depends on the configuration interface that provides some configuration parameters like endpoints, tokens, etc.

API client class:

class ApiClient
{
    private $apiConfiguration;

    public function __construct(ApiConfigurationInterface $apiConfiguration)
    {
        $this->apiConfiguration = $apiConfiguration;
    }

API configuration interface:

interface ApiConfigurationInterface
{
    public function getToken();
    public function getEndpoint();
    public function getProxy();
}

In the service provider I bind appropriate class to this interface:

    public function register()
    {
        $this->app->bind(ApiConfigurationInterface::class, ApiConfiguration::class);
    }

And it works fine.

Also, I wrote some tests for my API client. In the tests I would like to substitute production configuration class to stub class that also implements configuration interface. This will enable me to provide API client fake parameters and API client will use mock external API providing directly in tests.

For achieve it, I added the condition to service provider that checks environment and binds different configuration interface implementations:

    public function register()
    {
        if ($this->app->environment('testing')) {
            $this->app->bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);
        } else {
            $this->app->bind(ApiConfigurationInterface::class, ApiConfiguration::class);
        }
    }

That's fake configuration class:

class FakeApiConfiguration implements ApiConfigurationInterface
{
    public function getToken()
    {
        return 'token';
    }

    public function getEndpoint()
    {
        return 'http://localhost:9999/endpoint';
    }

    public function getProxy()
    {
        return null;
    }
}

In the test class I use service container to inject API client.

class ApiClientTest extends TestCase
{
    use HttpMockTrait;

    private $apiClient;

    ...

    public function setUp()
    {
        $this->setUpHttpMock();

        app()->make(ApiClient::class);

    }
}

And that's the problem: when I try to perform tests, PHPUnit fails with next messag:

Illuminate\Contracts\Container\BindingResolutionException : Target [App\Services\ApiClients\ApiClient\Configuration\ApiConfigurationInterface] is not instantiable while building [App\Services\ApiClients\ApiClient\ApiClient].

I tried to delete conditions from service provider to check if they are the source of the problem:

    public function register()
    {
        $this->app->bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);
    }

But I have no changes. PHPUnit message is the same.

I tried also dirty trick: create TestClass instance in the test controller and call test from here. And it works without errors.

class TestController extends Controller
{
    public function test()
    {
        $test = new ApiClientTest();
        ApiClientTest::setUpBeforeClass();
        $test->setUp();
        $test->someTest();
    }
}

I cannot understand why service container does not bind and instantiate appropriate class to API configuration interface while running tests. I spent a lot of time to solve that problem. But could not find a resolution.

Please help to solve it.

0 likes
9 replies
Scooby's avatar

You have an additional "i" here..

iinterface ApiConfigurationInterface
Rooduef's avatar

Sorry, It just a typo while I was formatting post. Real code does not contain it.

Sti3bas's avatar
Sti3bas
Best Answer
Level 53

@rooduef

  1. I would not recommend to have tests related code in your production code.
  2. You have to call parent::setUp() when overriding setUp method.

AppServiceProvider:

public function register()
{
    $this->app->bind(ApiConfigurationInterface::class, ApiConfiguration::class);
}

Test:

class ApiClientTest extends TestCase
{
    use HttpMockTrait;

    private $apiClient;

    public function setUp()
    {
        parent::setUp();
        $this->setUpHttpMock();

        $this->app->bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);

        $this->apiClient = $this->app->make(ApiClient::class);
    }

    /** @test */
    public function example()
    {
        dd($this->apiClient);
    }
}

Result after running example test:

App\ApiClient^ {#355
  -apiConfiguration: Tests\FakeApiConfiguration^ {#356}
}
2 likes
Rooduef's avatar

Thank you so much!

I didn't even think that I can perform bindings in setUp() method :)

After I bind FakeApiConfiguration::class to ApiConfigurationInterface at setUp() method, it wors fine.

But instead of $this->app->bind(...); I perform binding like this: app()->bind(...);. Because first approach ends with Error : Call to undefined function Tests\Unit\ApiClient\bind(). Can you also tell why it is and which approach is proper?

Thank you again!

Sti3bas's avatar

But instead of $this->app->bind(...); I perform binding like this: app()->bind(...);. Because first approach ends with Error : Call to undefined function Tests\Unit\ApiClient\bind().

Have you added parent::setUp(); at the top of setUp method?

Can you also tell why it is and which approach is proper?

There is no proper way, it depends on what you prefer. Both does the same thing, app() is just a global helper to access application instance.

1 like
Rooduef's avatar

Have you added parent::setUp(); at the top of setUp method?

Yep. That's my setUp() method:

public function setUp()
    {
        parent::setUp();

        $this->setUpHttpMock();

        $this->app-bind(ApiConfigurationInterface::class, FakeApiConfiguration::class);

        $this->apiClient = $this->app->make(ApiClient::class);
    }

Both does the same thing, app() is just a global helper to access application instance.

I think that the clear way is don't use global helper... But I can't call it via $this object.

Sti3bas's avatar

Yep.

Strange.

Are you extending from the correct TestCase? It should be Tests\TestCase.

Maybe you have setUp override in your Tests\TestCase class which doesn't call parent::setUp();?

Rooduef's avatar

It should be Tests\TestCase.

Hm... The reason is that I extended my test class with PHPUnit\Framework\TestCase instead of Tests\TestCase. Now I extend my class from Tests\TestCase and it got the desired result.

But now I don't understand which and why parent class I should use :) And is out of this topic. I must research it independently. But I would be grateful if you give me some explanation about this.

Anyway, you gave me some useful and non-evident information and save a lot of my time. Thank you.

Please or to participate in this conversation.