vaites's avatar
Level 1

Pest datasets loading data from database or service container

I'm trying to test some URLs on my application, that must be accesed using different roles. I need the list of pages to be loaded from a database and from an Statamic collection. I have a singleton for that, and I want to use it as a dataset:


// returns a \Illuminate\Support\Collection instance
dataset('example', fn() => app('example')->keyBy('uuid'));

The problem is that I get this error:

Target class [example] does not exist

I checked that my service provider is called an the singleton is binded. Why can't I use it as a dataset?. I can use it inside the test itself, but I need to call the test for each item, not iterate the collection inside the test (multiple assertions are needed).

I tried to query models directly using User::query()->all() but I get the following error:

Call to a member function connection() on null

And yes, my tests extends Tests\TestCase class...

What I'm doing wrong? Thanks!

0 likes
7 replies
RemiM's avatar

Since dataset() runs before Laravel is fully booted, try moving the singleton call inside a beforeEach() hook:

beforeEach(function () {
    $this->example = app('example')->keyBy('uuid');
});

If you want to keep using Pest datasets, you can wrap dataset() inside a uses() block to ensure the application is fully booted:

uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class);

dataset('example', function () {
    return app('example')->keyBy('uuid'); // Now app() will work because the app is booted
});

it('tests URLs with different roles', function ($data) {
    // Your test logic
})->with('example');

vaites's avatar
Level 1

Thanks @remim, but none of the options solves the problem...

Assume a singleton defined on AppServiceProvider::boot() method:

<?php declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->singleton('example', fn() => ['foo', 'bar', 'baz']);
    }
}

The first example doesn't change anything because already can use app('example') inside my test:

<?php declare(strict_types=1);

uses(Tests\TestCase::class);

beforeEach(function () 
{
    $this->example = app('example')->keyBy('uuid');
});

test('with before each', function()
{
	// works OK
	expect($this->example)->each->toBeInstanceOff(\App\Models\Example::class);

	// is the same as previous and works 
	expect(app('example'))->each->toBeInstanceOff(\App\Models\Example::class);
});

The point here is to create a reusable dataset, and this doesn't work. I can use the singleton inside the test itself but not in the dataset:

<?php declare(strict_types=1);

uses(Tests\TestCase::class);

dataset('example', function()
{
    // throws 'Call to undefined method Illuminate\Container\Container::isBooted()'
    $booted = app()->isBooted();

    // throws 'Target class [example] does not exist.'
    return app('example')->keyBy('uuid');
});

it('working example', function()
{
    app()->isBooted(); // true

    expect(app('example'))->each->toBeString();
});

it('failing example', function(string $text)
{
    expect($text)->toBeString();
})
->with('example');

There is something I don't understand: I checked that the boot method of the AppServiceProvider class, where I set the example singleton, is called before the dataset is loaded. Why is not available?

Anyway, I want a reusable dataset for two reasons:

  • It will be use across tens of tests: need to create a lot of tests based on these collections with multiple roles involved
  • The output is different: using expect()->each Pest only shows one line, and with a dataset shows one line per item

So I understand that I cant create a dataset that depends on a fully booted application

Thanks for your help!

RemiM's avatar

@vaites Maybe you can try using a Lazy-Loading Dataset

Instead of evaluating the dataset too early, return a closure that fetches the singleton inside the test execution when the app is fully booted.

dataset('example', function () {
    return fn() => app('example')->keyBy('uuid');
});

it('failing example', function ($datasetLoader) {
    $dataset = $datasetLoader(); // Resolve the dataset inside the test
    expect($dataset)->each->toBeString();
})->with('example');

vaites's avatar
Level 1

@RemiM Thanks, but the problem is the same. The test isn't called once per collection item, it's called only once and has the iteration inside. It's exactly the same thing as using the singleton inside the test.

Will need a new approach, because if seems too complex for something that must be very simple...

RemiM's avatar

@vaites I see. In that case, instead of using a closure that returns a callable (fn()), try directly returning the resolved dataset as an array.

Here’s how you can modify the code:

dataset('example', function () {
    return app('example')->keyBy('uuid')->toArray(); // Directly return the array here
});

it('tests URLs with different roles', function ($datasetItem) {
      // $datasetItem will be an individual item from the dataset
    expect($datasetItem)->toBeString();
})->with('example');

Hopefully, this will resolve your issue.

vaites's avatar
Level 1

@RemiM That was my first choice, but it brings us back to the origin of the question, since using app('example') in the dataset causes the error Target class [example] does not exist.

I guess I'll have to settle for doing a test with each and not having the desired output, even if it conditions the way I design the tests.

raihanrazon's avatar

If you are not using RefreshDatabase or anything to seed/refresh database every time during testing, you can follow this approach -

1- Create a boot() function in tests/Pest.php

/**
 * A generic boot function for Laravel applications.
 *
 * @return mixed
 */
function boot()
{
    $app = require __DIR__.'/../bootstrap/app.php';
    $app->make(Kernel::class)->bootstrap();
    restore_error_handler();
    restore_exception_handler();

    return $app;
}

2- Use the function inside your dataset before making a DB query

dataset('ids', function () {
    boot();
    return User::where('is_active', 1)->pluck('id')->toArray();
});

3- Use the dataset in your test.

test('data loading', function ($ids) {
    expect($detectableId)->toBeInt();
})->with('ids');

Please or to participate in this conversation.