JorickL's avatar

Swap class when running test?

In my Laravel 5.7 I want to 'fake' an API endpoint of a service we use. I've created a 'Fake class' which acts as the API. In my models I whip up a new connection to the external API like this:

$connection = (new TicketAPI)->bookings($attributes);

This works, like a charm.

Now, I want to replace, or 'swap' the TicketAPI::class when I'm running tests. So, I thought I could use

$fakeTicket = new FakeTicket;
$this->app->instance(TicketAPI::class, $fakeTicket);

But it still reaches the TicketAPI::class. Is someone able to point me in the right direction, or am I trying to use the $this->app->instance() method in the complete wrong way? Is it even possible to swap classes like this?

0 likes
7 replies
Talinon's avatar
Talinon
Best Answer
Level 51

You are on the right track.

$connection = (new TicketAPI)->bookings($attributes);

The problem with the above statement is that you are still directly "newing up" an instance of TicketAPI. What you want to do, is something like this:

$connection = resolve(TicketAPI::class)->bookings($attributes);

Or, better yet, look into swapping out your underlying service class by coding them to an interface. When you code to an interface, you can swap out the underlying implementation by updating a single binding (usually within a service provider)

For example:

ServiceProvider:

$this->app->bind(TicketInterface::class, TicketAPI::class);

Anywhere within your app that needs it:

$connection = resolve(TicketInterface::class)->bookings($attributes);

Test:

$fakeTicket = new FakeTicket;
$this->app->instance(TicketInterface::class, $fakeTicket);
3 likes
JorickL's avatar

@TALINON - Thanks! I'll will try this! About the interfaces: both classes (TicketAPI and FakeTicket) implement a TicketGatewayInterface. That holds two functions:

<?PHP

namespace App\Tickets;

interface TicketGatewayInterface 
{
    public function bookings();

    public function notes();
}

When I tried to use the interface like this:

$connection = (new TicketGatewayInterface)->bookings($attributes);

It shouted something like "can't instantiate interface" etc. I tried putting it in the models constructor (which before that didn't exist) and then it threw an error that the constructor attributes expected 2 arguments and only one was given.

Then I lost it and reversed everything back from where I started and asked Laracast for a bright light! ;-)

JorickL's avatar

@TALINON - Yikes! Didn't saw your edit: that's... Brilliant. I tried similar, but without the resolve() in my model. Will try that again... I'll let you know!

Thanks in advance!

Talinon's avatar

You are still trying to instiantiate (new up) the class directly. Since that is not an interface, it is squawking because you can't instantiate an interface.

You need to place in your service provider:

$this->app->bind(TicketGatewayInterface::class, TicketAPI::class);

Then, instantiate your underlying implementation like so:

$connection = resolve(TicketGatewayInterface::class)->bookings($attributes);
1 like
JorickL's avatar

@TALINON - Yes, thanks! What I said, I tried before the way I described earlier, but not like you suggested. I will try your way tomorrow morning and will come back to you to let you know if it worked (and mark your answer as best...).

JorickL's avatar

@TALINON - Thank you, very much! I've got it working now. Only, I had this line:

$this->app->bind(TicketInterface::class, TicketAPI::class);

placed in my AppServiceProvider::register() method. WRONG! When testing and dd() the bookings() method on both of my Classes, it keep going to the TicketAPI::class, where I wanted the FakeTicket::class to be called. Then, I remembered something about the method priorities in a ServiceProvider and thought: well, maybe I should put it in the boot() method of my AppServiceProvider...

BINGO It worked! Thanks a lot, Talinon!

1 like

Please or to participate in this conversation.