JustSteveKing / laravel-transporter

Transporter is a futuristic way to send API requests in PHP. This is an OOP approach to handling API requests.
MIT License
457 stars 26 forks source link

Question: how to test with fake reponses in Laravel tests? #25

Closed vlradstake closed 2 years ago

vlradstake commented 2 years ago

I'm trying to fake some data in my project tests. But Http::fake() doesn't seem to work, is there a why to fake the data?

AbdullahFaqeir commented 2 years ago

For Normal Request

TestRequest::fake(
    status: 200,
)->withToken('foobar')
->withData([
    'title' => 'Build a package'
])->withFakeData([
    'data' => 'faked'
])->send();

For Concurrency

$responses = Concurrently::fake()->setRequests([
    TestRequest::fake()->setPath(
        path: '/todos/1',
    )->as(
        key: 'first'
    ),
    TestRequest::fake()->setPath(
        path: '/todos/2',
    )->as(
        key: 'second'
    ),
    TestRequest::fake()->setPath(
        path: '/todos/3',
    )->as(
        key: 'thirds'
    ),
])->run();
JustSteveKing commented 2 years ago

For Normal Request

TestRequest::fake(
    status: 200,
)->withToken('foobar')
->withData([
    'title' => 'Build a package'
])->withFakeData([
    'data' => 'faked'
])->send();

For Concurrency

$responses = Concurrently::fake()->setRequests([
    TestRequest::fake()->setPath(
        path: '/todos/1',
    )->as(
        key: 'first'
    ),
    TestRequest::fake()->setPath(
        path: '/todos/2',
    )->as(
        key: 'second'
    ),
    TestRequest::fake()->setPath(
        path: '/todos/3',
    )->as(
        key: 'thirds'
    ),
])->run();

This is 👌

The idea was that the only thing you need to swap is build() to fake() which accepts an optional status code for you to return, for bad path and alternative status code testing. Then you can also fake the response data coming back with withFakeData() if you want/need to at all.

vlradstake commented 2 years ago

@JustSteveKing @AbdullahFaqeir So this example should work? Maybe i am doing something wrong but if i run this test TestRequest::build()->send()->json() returns the real data and not the fake data.


class ExampleControllerTest extends TestCase
{

    public function test_it_can_fake_data()
    {
        TestRequest::fake()
            ->withFakeData(
                ['data' => 'faked']
            )->send();

        $response = $this->get('/');
    }
}
JustSteveKing commented 2 years ago

@vlradstake do you have a repo or gist for the request you are trying to send? I would need to see the actual Transporter request to know what to do - but feel free to strip out anything sensitive or business critical.

All you should need to do in theory is switch the build method for a fake method, and add an array in withFakeData for the response body you want back

vlradstake commented 2 years ago

@JustSteveKing sorry took some time, had to check some things first. This is the code from the application, is this enough?


class CraftCmsRestRequest extends \JustSteveKing\Transporter\Request
{
    public function __construct(HttpFactory $http)
    {
        $this->baseUrl = config('transporter.craft_cms.base_uri');
        $this->baseUrl .= '/api';
        parent::__construct($http);
    }

    protected function withRequest(PendingRequest $request): void
    {
        $request->contentType('application/json');
    }
}

class ListRetour extends CraftCmsRestRequest
{
    protected string $method = 'GET';

    protected string $path = '/redirects';

    protected function withRequest(PendingRequest $request): void
    {
        $this->withQuery(
            [
                'domain' => request()->getSchemeAndHttpHost(),
                'siteHandle' => MultiSite::getConfig('craft.siteHandle'),
            ]
        );
        parent::withRequest($request);
    }
}

class ResolvePageTest extends TestCase
{

    public function test_it_can_resolve_data()
    {

        ListRetour::fake(status: 200)->withFakeData(
            [
                'test' => 'test'
            ]
        )->send();

        $data = $this->app->make(SyncRetourList::class)->execute();
    }

}

class SyncRetourList
{
    public function execute(): void
    {
        $list = ListRetour::build()->send()->json();
// i would expect the fake data here, but i get the real api data.
        $cache = CacheHelper::getCacheStore('craft.redirects');
        $cacheKey = CacheHelper::getCmsCacheKey('redirects');
        $cache->forever($cacheKey, $list);
    }
}
JustSteveKing commented 2 years ago

@vlradstake you issue seems to be here:

class SyncRetourList
{
    public function execute(): void
    {
        $list = ListRetour::build()->send()->json(); // <--
    }
}

Transporter is designed to swap build() with fake() to fake the request, so what this class is doing, isn't actually faking the request

vlradstake commented 2 years ago

@JustSteveKing ok, i understand. So how can i fake the data in the test? When i use the Laravel Htttp client i can add Http::fake in the test to make this work.

Another way is to write something like this, but this feels wrong :p

class SyncRetourList
{
    public function execute(): void
    {
        if(app()->environment('testing')) {
            $list = ListRetour::fake()->send()->json();
        } else {
            $list = ListRetour::build()->send()->json();
        }
    }
}
JustSteveKing commented 2 years ago

This is something that you will have to figure out what works for you, in this situation I would approach things slightly differently. I would test that my transporter tests are working, and then this class I would just test that it doesn't throw any exception.

You have 2 bits of logic here, the request and the wrapping class. Do you need to test that the wrapping class calls the code it is supposed to? Or do you just test the request itself.

If this were my code I would:

class SyncRetourList
{
  public function execute(): void
  {
    $response = ListRetour::build()->send();

    if ($response->failed()) {
      throw $response->toException();
    }
  }
}

As this class isn't actually returning anything you either want to send a request or want to throw a HTTP exception. You could try something like:

class SyncRetourList
{
  public function execute(string $method = 'build', null|array $data = null): void
  {
    $request = ListRetour::$method();

    if (! is_null($data) && $method === 'fake') {
      $request->withFakeData($data);
    }

    $response = $request->send();

    if ($response->failed()) {
      throw $response->toException();
    }
  }
}

Which would allow you to:

(new SyncRetourList)->execute('fake')
vlradstake commented 2 years ago

@JustSteveKing Thanks for helping out! Found a way to fake the data in the tests:


class ResolvePageTest extends TestCase
{

    public function test_it_can_resolve_data()
    {
        $this->app
            ->when(ListRetour::class)
            ->needs(HttpFactory::class)
            ->give(function () {
                return (new HttpFactory())->fake(function ($request) {
                    return Http::response(['data' => 'here'], 200);
                });
            });

        $data = $this->app->make(SyncRetourList::class)->execute();
    }

}
JustSteveKing commented 2 years ago

That's really helpful to know! Thanks @vlradstake - I may add that to the readme for others 🙂