Search code examples
phplaraveleloquentnested-routeslaravel-testing

Laravel Package - Experiencing different data of model between in the test file and in the controller


I'm currently working on a laravel package that has route configuration below

Route::apiResource('companies', Controllers\CompanyController::class);
Route::apiResource('companies.addresses', Controllers\AddressController::class)->parameter('companies', 'entity');

Route::apiResource('employees', Controllers\EmployeeController::class);
Route::apiResource('employees.addresses', Controllers\AddressController::class)->parameter('companies', 'entity');

here is the controllers

// Http/CompanyController.php

class CompanyController extends Controller
{
    // index, store, show, update, destroy methods are here
}

// Http/EmployeeController.php

class EmployeeController extends Controller
{
    // index, store, show, update, destroy methods are here
}

// Http/AddressController.php

use My\Contracts\Entity;
use My\Http\Requests\AddressStoreRequest;

class AddressController extends Controller
{
    public function index(Entity $entity)
    {
        $items = $entity->addresses()->latest();

        // the code
    }

    public function store(AddressStoreRequest $request, Entity $entity)
    {
        // the code
    }

    // show, update, destroy methods are here
}

here is my models

class Company extends Model implements Entity
{
    public function addresses()
    {
        return $this->morphMany(Address::class, 'owner');
    }
}

class Employee extends Model implements Entity
{
    public function addresses()
    {
        return $this->morphMany(Address::class, 'owner');
    }
}

class Address extends Model
{
    // ...
}

and here is inside ServiceProvider

use My\Models\Company;
use My\Models\Employee;
use My\Contracts\Entity;

$this->app->bind(Entity::class, function ($app) {
    return $app->make($app['router']->is('companies.*') ? Company::class : Employee::class);
});

Noticed that the Entity is actually a interface that bound to a model depending on the route name since the AddressController is used in two different routes.

And last one here my test

// the EmployeeTest did pretty much the same
class CompanyTest extends TestCase
{
    #[Test]
    public function should_able_to_retrieve_all_data(): Company
    {
        $models = Company::factory(2)->create();

        $response = $this->getJson('base/companies');

        // the assertion goes here

        return $models->first();
    }

    #[Test]
    #[Depends('should_able_to_retrieve_all_data')]
    public function should_able_to_retrieve_all_addresses(Company $model): void
    {
        $response = $this->getJson("base/companies/{$model->getRouteKey()}/addresses");

        $response->assertOk();
    }
}

Every other test runs just fine except the should_able_to_retrieve_all_addresses one. It always returns 404. Initially I thought it was about the implicit model binding, after spending couple hours digging I'm pretty sure it wasn't.

Up until I add dump(Company::all()) to my CompanyTest::should_able_to_retrieve_all_addresses() and AddressController::index() method like these

class CompanyTest extends TestCase
{
    #[Test]
    #[Depends('should_able_to_retrieve_all_data')]
    public function should_able_to_retrieve_all_addresses(Company $model): void
    {
        $response = $this->getJson("base/companies/{$model->getRouteKey()}/addresses");
        dump(Company::all()->toArray());

        $response->assertOk();
    }
}

class AddressController extends Controller
{
    public function index(Entity $entity)
    {
        dump(Company::all()->toArray());
    }
}

I found that the one from test file can returns all the companies available, but the one from the controller return an empty array. How it could even possible?

You can find the repository here


Solution

  • Spent another hour and figured out that all I need is invoke model factory in every single test method, instead of using test dependencies, like this

    #[Test]
    public function should_able_to_retrieve_all_addresses(): void
    {
        $model = Company::factory()->createOne();
    
        $response = $this->getJson("base/companies/{$model->getRouteKey()}/addresses");
    
        $response->assertOk();
    }
    

    also I have to update the container entry like this to resolve the route binding myself, instead of relying on implicit model binding

    $this->app->bind(Entity::class, function ($app) {
        $router = $app->make('router');
        $entity = $app->make($router->is('companies.*') ? Company::class : Employee::class);
    
        return $entity->resolveRouteBinding($router->input('entity')) ?: $entity;
    });
    

    And indeed it works! But, why? I have no idea.