Options::fake() swaps the manager for an in-memory store: nothing touches the database, and every write is recorded. It returns the fake (RoundlyConsulting\Options\Testing\OptionsFake, a subclass of OptionsManager) for assertions; each assertion also works statically, as Options::assertSet(…):
use RoundlyConsulting\Options\Facades\Options;
it('records writes from every entry point', function () {
$fake = Options::fake();
$user = User::factory()->create();
Options::set(ThemeOption::class, 'dark'); // the facade
$user->option(ThemeOption::class)->set('light'); // the HasOptions trait
LocaleOption::for($user)->forget(); // an option instance
expect(Options::get(ThemeOption::class))->toBe('dark'); // reads come from memory
$fake->assertSet(ThemeOption::class, 'dark'); // global scope
$fake->assertSet(ThemeOption::class, 'light', $user); // the user's scope
$fake->assertSet(ThemeOption::class); // set to anything, globally
$fake->assertForgotten(LocaleOption::class, $user);
$fake->assertNothingImported();
});
it('records imports', function () {
Options::fake();
Options::import('[{"key": "theme", "value": "dark"}]');
Options::assertImported(); // static form
Options::assertImported(fn (array $payloads) => count($payloads) === 1); // list<OptionPayload>
});
it('changes nothing on a read-only page', function () {
$fake = Options::fake();
$this->get('/dashboard')->assertOk();
$fake->assertNothingSet();
$fake->assertNothingForgotten();
$fake->assertNothingImported();
});Assertions
| Assertion | Passes when |
|---|---|
assertSet($option, $value = null, $owner = null) | The option was set in that scope (null = global) — to $value as you passed it, or to anything when $value is null. |
assertNothingSet() | No option was set at all. |
assertForgotten($option, $owner = null) | The option was forgotten (or reset) in that scope. |
assertNothingForgotten() | No option was forgotten or reset. |
assertImported(?Closure $callback = null) | An import() happened — optionally one whose list<OptionPayload> passes the callback. |
assertNothingImported() | Nothing was imported. |
What the fake covers
Every value operation funnels through the manager, so the fake sees all of them: the facade, an OptionsManager resolved after the call, the for() / option() / key() / group() handles, option instances such as ThemeOption::for($user)->set(), the HasOptions trait, the options() and setting() helpers and @option. It behaves like the real manager where a test would notice:
- Writes are validated against rules() — an invalid one throws and isn’t recorded.
- Reads return the cast type, exactly as with the database; setMany() and group writes validate the whole batch first.
- export() and exportJson() read the in-memory store with the real scoping rules; it holds what the database would — raw strings, ciphertext for an encrypted option. import() is recorded and loaded into it.
- Keys you registered with Options::register() before calling fake() still resolve.
- Access control and observers still run; events aren’t dispatched and caches aren’t used.
- assertSet() compares the value as you passed it, not the cast one.
Database-backed tests
The in-request memo is bound in the container, so each test’s fresh application starts with an empty one. The persistent cache lives in your cache store — if tests share a store that outlives the application (file, Redis), flush it in beforeEach:
use RoundlyConsulting\Options\Facades\Options;
use RoundlyConsulting\Options\Option;
beforeEach(fn () => Options::flushCache());
it('reads a seeded global value', function () {
Option::factory()->global()->create(['key' => 'theme', 'value' => 'dark']);
expect(Options::get(ThemeOption::class))->toBe('dark');
});Factory states
use RoundlyConsulting\Options\Option;
Option::factory()->global()->create(['key' => 'theme', 'value' => 'dark']);
Option::factory()->forOwner($user)->value('light')->create(['key' => 'theme']);
Option::factory()->withMeta(['source' => 'seed'])->create();The factory writes value as-is, so store the serialized form for options with a non-string cast.
Package test suite
composer testShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.