Skip to content

Conversation

@SanderMuller
Copy link
Contributor

@SanderMuller SanderMuller commented Nov 14, 2025

This addition is a follow-up to #57663 and makes it possible to use WithCachedConfig in the base TestCase and disable it for certain tests that override the ENV or Config.

For example for these two test examples which fail without the disableConfigCache:

<?php declare(strict_types=1);

namespace Tests\Unit;

use Illuminate\Contracts\Encryption\DecryptException;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class EncrypterTest extends TestCase
{
    private string $originalAppKey;
    private string $oldAppKey;

    protected function setUp(): void
    {
        parent::setUp();

        $this->disableConfigCache();

        $this->oldAppKey = 'A6Ic4cFVOr5veD9TferaIhgDyNnqz3eq';
        /** @phpstan-ignore disallowed.function */
        putenv('APP_PREVIOUS_KEYS=' . $this->oldAppKey);
        $this->originalAppKey = config('app.key');
    }

    protected function tearDown(): void
    {
        $this->setAppKeyAndClearEncrypterInstance($this->originalAppKey);

        parent::tearDown();
    }

    #[Test]
    public function it_can_encrypt_and_decrypt_using_regular_app_key(): void
    {
        $payload = fake()->text();

        $encrypted = encrypt($payload);

        self::assertNotSame($encrypted, $payload);

        $decrypted = decrypt($encrypted);

        self::assertSame($decrypted, $payload);
    }

    #[Test]
    public function it_can_decrypt_payload_with_old_app_key(): void
    {
        $payload = fake()->text();

        $this->setAppKeyAndClearEncrypterInstance($this->oldAppKey);

        $encrypted = encrypt($payload);

        self::assertNotSame($encrypted, $payload);

        $this->setAppKeyAndClearEncrypterInstance($this->originalAppKey);

        $decrypted = decrypt($encrypted);

        self::assertSame($decrypted, $payload);
    }

    #[Test]
    public function decrypting_payload_with_invalid_old_app_key_throws_exception(): void
    {
        $payload = fake()->text();

        $this->expectException(DecryptException::class);

        $incorrectOldAppKey = 'd5So7vPWLw9dnR2UfswmJidBpEhyr5tq';

        $this->setAppKeyAndClearEncrypterInstance($incorrectOldAppKey);

        $encrypted = encrypt($payload);

        self::assertNotSame($encrypted, $payload);

        $this->setAppKeyAndClearEncrypterInstance($this->originalAppKey);

        decrypt($encrypted);
    }

    private function setAppKeyAndClearEncrypterInstance(string $appKey): void
    {
        config()->set('app.key', $appKey);

        app()->forgetInstance('encrypter');
    }
}

And

<?php declare(strict_types=1);

namespace Tests\Unit;

use Illuminate\Foundation\Console\PackageDiscoverCommand;
use Illuminate\Support\Env;
use Illuminate\Testing\PendingCommand;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class CreateProjectTest extends TestCase
{
    private string $appKey;

    protected function setUp(): void
    {
        parent::setUp();

        $this->disableConfigCache();

        $this->appKey = config('app.key');

        Env::enablePutenv();

        /** @phpstan-ignore disallowed.function */
        putenv('APP_KEY=');
        $_SERVER['APP_KEY'] = null;
        $_ENV['APP_KEY'] = null;

        $this->refreshApplication();
    }

    protected function assertPreConditions(): void
    {
        self::assertEmpty(config('app.key'));
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        /** @phpstan-ignore disallowed.function */
        putenv('APP_KEY=' . $this->appKey);
        $_SERVER['APP_KEY'] = $this->appKey;
        $_ENV['APP_KEY'] = $this->appKey;
    }

    #[Test]
    public function it_can_discover_packages_without_an_app_key_set(): void
    {
        $pendingCommand = $this->artisan(PackageDiscoverCommand::class);

        if (! $pendingCommand instanceof PendingCommand) {
            throw new \RuntimeException('Expecting to mockConsoleOutput');
        }

        $pendingCommand->assertSuccessful();
    }
}

@github-actions
Copy link

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@SanderMuller SanderMuller changed the title Add disableConfigCache method [12.x] Add disableConfigCache method to the WithCachedConfig trait Nov 14, 2025
@SanderMuller
Copy link
Contributor Author

@cosmastech could you shine your light on this PR, would this be a logical approach to achieving this functionality?

{
$this->app->instance('config_loaded_from_cache', false);

CachedState::$cachedConfig = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you want to remove the current cached config, since the next test which wants a cached config will have to build it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why, but without this my tests still fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you ever envision even one test in a class still wanting the original cached config?

While I know it's not pretty, I wonder if you could just override setUpWithCachedConfig() and markConfigCached() for the entire test class, making them no-ops?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you ever envision even one test in a class still wanting the original cached config?

Potentially, though it's possible to isolate these kind of tests.
But I can also imagine more Laravel devs having the need to have some kind of config testing, which conflicts with this caching, but still wanting to by-default cache the config, and having an option to disable it for a single test. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This technically works, but the devex feels a little unusual, and it's got some footguns in it.

  1. If you don't need the cache, we shouldn't build it at all. We definitely shouldn't clear the memo of cached config, since it's going to delay the tests after it which need to rebuild it. This might be surprising to some devs since calling this method will make at least 1 subsequent test slower as it rebuilds the cache.
  2. Having to call this method in the setUp feels weird, since that means every test in your file is going to be under the same constraints. In which case, just making those methods no-ops is probably easiest (or just not applying the trait to this particular test, which may be a pain since they're children of your root test).

We could add a property check like this, but it doesn't really offer the flexibility to set it on a per-test basis since it happens in TestCase@setUp():

image

You might be better off just adding a method like:

protected function withFreshConfig(): void
{
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::alwaysUse(null);
    $this->app->make(\Illuminate\Foundation\Bootstrap\LoadConfiguration::class)->bootstrap($this->app);
}

With that method, you could call it at the start of each test method, rather than in the test setUp.

Let me know your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree about the devex, and think the ->withFreshConfig() API feels much better.

The proposed implementation worked for most of my use-cases, however not all. It seems that this doesn't refresh the config of packages (I guess it doesn't merge them in), at least I am getting an exception when running a test where config('request-factories') returns null in the following snippet:

namespace Worksome\RequestFactories;

...

final class RequestFactoriesServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        ...
        $this->app->singleton(Finder::class, fn () => new ConfigBasedFinder(config('request-factories')));
    }

If I call $this->refreshApplication(); after $this->withFreshConfig() then the test passes again (not sure if that info helps)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense. Config is merged when the Provider is booted, which would explain why it's not working that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants