saloonphp / saloon

🤠 Build beautiful API integrations and SDKs with Saloon
https://docs.saloon.dev
MIT License
2.08k stars 106 forks source link

Extra Mocking Documentation #259

Closed JonPurvis closed 1 year ago

JonPurvis commented 1 year ago

Hey @Sammyjo20 👋

I finally got round to giving Saloon a try, absolutely amazing work, I can definitely see it coming in useful for several of the projects I work on.

I was wondering if you'd be able to add some extra documentation to the Mocking section? Specifically how you would take the MockClient, (for example, I'm mocking one of my requests) and then use it in a test in a Laravel application? I'm currently rewriting some of my tests so they work with Saloon and I'm hitting a couple of issues, so it'd be good to see how you'd do it.

I'm 99.9% sure it's me being silly, although I haven't actually really touched mocking inside Laravel, other than the fake() functionality inside the HTTP Client.

Thanks in advance! 🤠

Sammyjo20 commented 1 year ago

Hey @JonPurvis many thanks for your kind messages and thank you for your patience, I have been a bit busy with work and home stuff - but hoping to get back into more Saloon stuff soon!

Yes absolutely, I will see where I can improve the docs on mocking. With Laravel though specifically, if you install the Laravel package composer require sammyjo20/saloon-laravel then it provides you with a facade that you can use to mock.

Traditionally, you have to pass the mock client into your connector for it to know to "fake" it:

$mockClient = new MockClient(...);

$connector = new ForgeConnector;
$connector->withMockClient($mockClient);

But the awesome thing about the Laravel package is you can use the Saloon facade to globally mock inside of a test. This is great because if you have some nested logic like an API call inside of a job, you don't have to pass a mock client all the way down.

For example:

Saloon::fake([
     // Your mock requests or fixtures
]);

$this->postJson('/forge/create-server')
->assertOk();

You can do any of the mock "methods" inside of Saloon::fake which are all listed here: https://docs.saloon.dev/testing/manual-fake-responses

JonPurvis commented 1 year ago

Hey @Sammyjo20

Thanks so much for taking the time to answer my query, I know how busy you are so didn't want to take up too much of your time. I've managed to get back round to looking at this and it's good to see I was on the right track rather than being completely wrong 😆

For some reason though, my "fake" response isn't getting returned, in my test I have:

Saloon::fake([
    TestRequest::class => MockResponse::make(['key' => 'value'], 200)
]);

$this->artisan('app:test-command');

In that command, I have:

$connector = new TestConnector;
$paginator = $connector->paginate(new TestRequest);

If I then dump $paginator->json(), I get:

Generator {#3635
    this: Saloon\Http\Paginators\PagedPaginator {#3634 …}
    trace: {
        ./vendor/sammyjo20/saloon/src/Http/Paginators/Paginator.php:82 { …}
        Saloon\Http\Paginators\Paginator->json() {}
    }
    closed: false
}

Am I correct in thinking that $paginator->json() should return what I've specified within the mock inside the test?

Another problem, and this one makes me think it's not using the Mock at all, if I loop through $paginator:

foreach ($paginator as $response) {
    $pageData = $response->json();
}

and dump $pageData, I actually get:

[
    "message" => "Unauthenticated"
}

But that shouldn't be the case if we're using the Mock, right?

Apologies for the information dump 😆 I just want to make sure I'm understanding it properly!

craigpotter commented 1 year ago

@JonPurvis I'm sure I had the same issue and I think it's the way pagination works if I remember rightly.

Check out how I did it here (granted not with the Laravel Facade but you should get an idea) https://github.com/craigpotter/fca-php-sdk/blob/cd40eb355b728dc3c351d69ae73c7bb050340207/tests/Feature/FirmResourceTest.php#L73

JonPurvis commented 1 year ago

Hey @Sammyjo20 and @craigpotter

Thanks again for taking the time to look over this, I'm still sure it's me being silly 😆 Sorry it took me so long to get back to it, I wanted to come back with a refreshed mind ready to tackle it.

I've decided to look into using the Saloon facade, as it is a Laravel app I'm working on, well, technically it's Laravel Zero, but it should still work.

Am I correct in thinking that Saloon::fake() works like Laravel's own Http::fake()? For example if I fake a response from a given URL using HTTP::fake(), and then run a command that inside it uses the HTTP client and goes to the same URL, the response will be whatever I'm faking in the test and doesn't actually hit the live URL. If this is the case, when I run:

it('completes successfully', function () {
    Saloon::fake([
        TestRequest::class => MockResponse::make(['key' => 'value'])
    ]);

    $this->artisan('command')
        ->assertSuccessful();
});

and in my command I have:

$testConnector = new TestConnector;
$response = $testConnector->send(new TestRequest);

dd($response->json());

When I run my test however, I actually get:

 Saloon\Http\Auth\TokenAuthenticator::__construct(): Argument #1 ($token) must be of type string, null given

But if I'm using the facade, it shouldn't need a token should it? In my TestConnector class, I'm setting it like so:

public function defaultAuth(): TokenAuthenticator
{
    return new TokenAuthenticator(config('services.token'));
}

Thanks!

craigpotter commented 1 year ago

Ahhh I believe the authenticator will still new up when the connector is initiated.

Try passing a fake token.


Config::set('services.token', 'test-token');

// or 

TokenAuthenticator(config('services.token', ''))
JonPurvis commented 1 year ago

I actually tried that before but gave me the same result 😢

return new TokenAuthenticator(config('services.token', ''));
Saloon\Http\Auth\TokenAuthenticator::__construct(): Argument #1 ($token) must be of type string, null given
craigpotter commented 1 year ago

Also @JonPurvis worth nothing that if your authenticator makes an api request, you will need to fake that as well.

You should be fine with token Auth but just putting that here as well

craigpotter commented 1 year ago

I actually tried that before but gave me the same result 😢

return new TokenAuthenticator(config('services.token', ''));
Saloon\Http\Auth\TokenAuthenticator::__construct(): Argument #1 ($token) must be of type string, null given

I would set it to a random string in your test and test that.

JonPurvis commented 1 year ago

Okay, weirdly closing PHPStorm and reopening it (WTF haha), now gives a different issue in the test:

array:1 [
  "message" => "Unauthenticated."
]

if your authenticator makes an api request, you will need to fake that as well.

I don't believe it does, all the authenticator does is gets the key from a config file, unless you mean something different 😄

craigpotter commented 1 year ago

Okay, weirdly closing PHPStorm and reopening it (WTF haha), now gives a different issue in the test:

array:1 [
  "message" => "Unauthenticated."
]

if your authenticator makes an api request, you will need to fake that as well.

I don't believe it does, all the authenticator does is gets the key from a config file, unless you mean something different 😄

Can you replicate it in a clean install of laravel and share it. Happy to try and help you 😊

JonPurvis commented 1 year ago

Hey @craigpotter,

Sorry for the delayed reply!

So, I've spent a couple more hours on this tonight. I copied my code into a Laravel application and was able to use the Saloon facade to fake a response and have that show in my test, which is exactly what I was after 🥳

I then spun up a new Laravel Zero application and did the exact same thing, except this time, running Saloon in a command worked fine, I was getting a 401 response which I expected as I had no API key in this application. When I ran Saloon as part of a test, it was giving me:

Target class [saloon] does not exist.

So, the issue I'm having seems to be because I'm using Laravel Zero. If I run php artisan on my Laravel application, I can see all of the available Saloon commands:

saloon
  saloon:auth               Create a new Saloon authenticator
  saloon:connector          Create a new Saloon connector class
  saloon:oauth-connector    Create a new Saloon OAuth2 connector class
  saloon:plugin             Create a new Saloon plugin
  saloon:request            Create a new Saloon request class
  saloon:response           Create a new custom Saloon response class

Laravel Zero, however, doesn't show this in the list of commands. I know if I install the PestPHP Laravel plugin, that then shows a couple of Pest commands to be used:

pest:dataset Create a new dataset file
pest:test    Create a new test file

So it looks like whilst Saloon itself works wonderfully, the Laravel plugin for it doesn't work with Laravel Zero applications.

I have my dummy Laravel Zero application I can put into a repo if you want to have a play around with it? I hope we can come up with a solution to this as it would be amazing to be able to fully utilise Saloon in Laravel Zero! I'm happy to help with testing and whatever else is needed!

Thanks

craigpotter commented 1 year ago

@JonPurvis Yeah share the repo and I will try and see if I can see the issue for you.

JonPurvis commented 1 year ago

Hey @craigpotter

Repo can be found here - https://github.com/JonPurvis/saloon-laravel-zero

I've replaced the API with a public API, as you wouldn't be able to connect to the API I'm connecting to in my application as it's locked down. I can still replicate the Facade issue with the public API though 😄

So if you run php saloon inspire, you'll see the output from the API, should be something like:

"{"fileSizeBytes":257170,"url":"https://random.dog/2bff25d0-c721-4078-8cc9-f3ce6b464428.jpg"}"

If you run ./vendor/bin/pest though, it should error with:

Target class [saloon] does not exist.

I'm looking forward to seeing what you find!

JonPurvis commented 1 year ago

Okay, after all that, I finally got it to work 😆 It's amazing what getting some sleep can do to the mind!

In AppServiceProvider.php, I needed to register SaloonServiceProvider. If this was actual Laravel, it would be registered automatically I believe.

/**
  * Register any application services.
*/
public function register(): void
{
    $this->app->register(SaloonServiceProvider::class);
}

Now whatever I define in Saloon::fake() is returned in my command when running a test. Available commands also now show in the artisan list with the others 🥳

Thanks for all your help with this @craigpotter, I've definitely learnt some things!

craigpotter commented 1 year ago

@JonPurvis Well done 👏

JonPurvis commented 1 year ago

@Sammyjo20 I'm going to close this issue now as my problem is resolved, but, would it be worth adding an extra bit of documentation for Laravel Zero to https://docs.saloon.dev/getting-started/installation? Mainly just to mention that you have to manually register the SaloonServiceProvider class?

Sammyjo20 commented 1 year ago

Hey @JonPurvis

Many thanks for letting me know! I didn't know Laravel Zero didn't auto-register service providers. I'll make sure to mention that!

Cheers Sam

Sammyjo20 commented 1 year ago

Update: I've added a little block of text here: https://docs.saloon.dev/official-plugins/laravel-integration

JonPurvis commented 1 year ago

Hey @Sammyjo20

Same! I think I just assumed they would, given Laravel itself does it 😆 I guess this is the reason I couldn't get a couple of other packages to work, I might have to revisit them now that I know what the problem may have been

Thanks very much for updating the documentation too, hopefully it helps out developers in the future!

🤠