PHPackages                             antonioprimera/laravel-test-scenarios - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. antonioprimera/laravel-test-scenarios

ActiveLibrary[Testing &amp; Quality](/categories/testing)

antonioprimera/laravel-test-scenarios
=====================================

A versatile package, allowing you to create and reuse test scenarios

v2.1.0(8mo ago)1237MITPHPPHP ^8.3

Since Apr 1Pushed 8mo ago1 watchersCompare

[ Source](https://github.com/AntonioPrimera/laravel-test-scenarios)[ Packagist](https://packagist.org/packages/antonioprimera/laravel-test-scenarios)[ RSS](/packages/antonioprimera-laravel-test-scenarios/feed)WikiDiscussions master Synced today

READMEChangelog (9)Dependencies (3)Versions (10)Used By (0)

Laravel Test Scenarios
======================

[](#laravel-test-scenarios)

This package allows you to easily create and reuse test scenarios in your project, and even publish test scenarios in your packages.

Overview
--------

[](#overview)

Test Scenarios allow you to create a set of models which resemble a live application, and reuse these scenarios in several tests. You can create any number of test scenarios, with different sets of models and application setup (e.g. application settings, configuration etc.).

For example, if you created a BlogScenario for a blog project with 2 users (editor, reader) and 2 posts (publishedPost and unpublishedPost), you could easily do something like this:

```
/** @test */
public function a_reader_can_only_read_published_posts()
{
    $scenario = new BlogScenario($this);
    $scenario->login($scenario->reader);

    $this->get(route('posts', ['slug' => $this->scenario->publishedPost->slug]))
        ->assertSuccessful();

    $this->get(route('posts', ['slug' => $this->scenario->unpublishedPost->slug]))
        ->assertStatus(404);
}
```

Installation
------------

[](#installation)

You can install this package to your project, in your dev dependencies via composer:

`composer require --dev antonioprimera/laravel-test-scenarios`

Overview and basic concepts
---------------------------

[](#overview-and-basic-concepts)

> Deterministic data is a hard requirement. Scenarios must create explicit, fixed fixtures (e.g. `admin@bforum.test`, `[SEC01] - Section 1`). Never rely on JSON outputs or random factories.

In order to create and use scenarios, you have to first create the "TestContext". The TestContext is nothing more than a fancy model factory, which should be smart enough to create models and adjust the settings and configuration of the application. In most cases, one TestContext is enough for an application.

After the TestContext is created, you can create any number of scenarios, using this context. Inside a TestScenario, in the Scenario Setup method, you will use the TestContext methods to create the models and adjust the configuration and application settings.

After you created the TestScenarios, you just instantiate them in your tests, and you can use the models created inside the scenarios and you can easily create more models, using the scenario context.

Creating the Scenarios
----------------------

[](#creating-the-scenarios)

### Step 1: create a TestContext for your project

[](#step-1-create-a-testcontext-for-your-project)

Create your TestContext class, in your `tests/Context` folder, by inheriting `AntonioPrimera\TestScenarios\TestContext`.

### Step 2: create your model factory methods

[](#step-2-create-your-model-factory-methods)

A TestContext will have various public methods, used as factories, allowing you to create models. For example, a TestContext for a Blog project would have methods like:

```
public function createPost(string $key, $author, $data = [])
{
    //allow the user to provide the author instance or its context key
    $authorInstance = $this->getInstance(User::class, $author, true);
    $post = $authorInstance->posts()->create($data);

    return $this->set($key, $post);
}

public function createComment(string $key, $parentPost, $author, $data = [])
{
    //allow the user to provide the post instance or its context key
    $postInstance = $this->getInstance(Post::class, $parentPost, true);

    //allow the user to provide the author instance or its context key
    $authorInstance = $this->getInstance(User::class, $author, true);

    $comment = $postInstance
        ->comments()
        ->create(array_merge($data, ['author_id' => $authorInstance->id]));

    return $this->set($key, $comment);
}
```

You should enable your methods to use either model instances or the context keys of models already created in the context.

In order to keep your TestContext class clean, you should group these methods in traits and just include them in your TestContext class. For the example above you would create two traits `tests/Context/Traits/CreatesPosts.php` and `tests/Context/Traits/CreatesComments.php`.

### Step 3: create your TestScenarios

[](#step-3-create-your-testscenarios)

You can create a TestScenario, in folder `tests/Scenarios`, by extending the abstract class `AntonioPrimera\TestScenarios\TestScenario` and implementing the 2 abstract methods **setup**and **createTestContext**.

The **createTestContext** must return the TestContext instance, which will be used to create the scenario models and data.

The **setup** method should use the previously instantiated Context to create all models for the scenario and assign them to corresponding keys (so they can be easily retrieved later).

Here's an example:

```
class SimpleBlogScenario extends TestScenario
{

    public function setup(mixed $data = null)
    {
        $context = $this->context;
        /* @var AppContext $context */

        //create all necessary models for this scenario
        $context->createUser('editor', ['role' => 'editor']);
        $context->createUser('reader', ['role' => 'reader']);
        $context->createPost('publishedPost', 'editor', ['published' => true]);
        $context->createPost('unpublishedPost', 'editor', ['published' => false]);
        $context->createComment('readerComment', 'publishedPost', 'reader', ['body' => 'Nice post']);
    }

    protected function createTestContext(?TestCase $testCase): TestContext
    {
        //the test context created previously for your own project
        return new BlogContext($testCase);
    }
}
```

> **Heads up:** When provisioning scenarios outside of PHPUnit / Pest you can omit the `$testCase`argument (`new BlogScenario()`), but any method that forwards to the underlying `TestCase`instance (assertions, `login()`, etc.) will now throw a descriptive exception so you immediately know that the scenario needs a real test case instance.

> **Docblocks help agents.** Add a short PHPDoc summary above every scenario describing the fixtures and actors it provisions. The `php artisan test-scenarios:list` command surfaces this text to other developers and AI agents.

Using Scenarios
---------------

[](#using-scenarios)

The models are created using the TestContext within the TestScenario and are handled by the TestContext. The TestScenario is a wrapper for the TestContext and forwards all attributes and methods via magic getter, magic setters and magic method calls to the TestContext.

In other words, the TestScenario exposes all created models and TestContext methods.

### Instantiating scenarios

[](#instantiating-scenarios)

In order to use a scenario, you must instantiate it, by providing the current test instance to its constructor.

```
$scenario = new SimpleBlogScenario($this);
```

Or you can instantiate it in your TestCase setup method.

```
protected SimpleBlogScenario $scenario;

protected function setUp(): void
{
    parent::setUp();
    $this->scenario = new SimpleBlogScenario($this);  //then just use $this->scenario in your tests
}
```

Or you can use a trait in your base TestCase, which automatically instantiates any typed scenario property in any of the inheriting TestCases, so you don't have to do anything other than declare the scenario property.

Here is how your base TestCase class could look like:

```
namespace Tests;

use AntonioPrimera\TestScenarios\HasScenarios;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication,
        HasScenarios;               //1. add this trait

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

        $this->setupScenarios();    //2. call this method to magically instantiate scenarios

        //... other setup stuff
    }
}
```

Here is how one of your application TestCase could look like:

```
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Scenarios\SimpleBlogScenario;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    protected SimpleBlogScenario $scenario;     //1. declare your scenarios with the proper type

    /** @test */
    public function a_moderator_can_edit_any_post()
    {
        $this->scenario->login('moderator');    //2. just use the scenario (it was magically set up)
        $this->patch(
            route('posts.update', ['post-id' => $this->scenario->post->id]),
            ['title' => 'Updated Title']
        );

        $this->assertEquals('Updated Title', $this->scenario->post->fresh()->title);
    }
}
```

### Accessing the models from a TestScenario

[](#accessing-the-models-from-a-testscenario)

When you create a model inside your TestScenario, you can create a property for each model created inside the Scenario class, but you can also use the TestContext `set($key, $valueOrModel)` method. This assigns a model or some piece of data to a key in the Context, which can be easily (magically) accessed on the Scenario instance.

You can then magically access your created models via the keys you assigned them in the TestScenario setup method. For example, if you created an admin user and assigned it to the `admin` key, you can access it via `$scenario->admin`.

Here's a test example, continuing the blog example above:

```
/** @test */
public function a_reader_can_only_read_published_posts()
{
    $scenario = new BlogScenario($this);
    $scenario->login($scenario->reader);

    $this->get(route('posts', ['slug' => $this->scenario->publishedPost->slug]))
        ->assertSuccessful();

    $this->get(route('posts', ['slug' => $this->scenario->unpublishedPost->slug]))
        ->assertStatus(404);
}
```

### Accessing TestContext methods from a TestScenario

[](#accessing-testcontext-methods-from-a-testscenario)

Any public methods you create in your TestContext, will be accessible on your TestScenario instance. For linting support from your IDE, you can also directly access the context instance on your scenario.

```
$scenario->createPost('somePost', ['title' => 'Some Title', 'body' => 'Some Body']);
//is equivalent to
$scenario->context->createPost('somePost', ['title' => 'Some Title', 'body' => 'Some Body']);

//then, to access the post you can do:
$post = $scenario->somePost;                    //this is the magic version
//or
$post = $scenario->context->get('somePost');    //this actually happens under the hood
```

### Login and Logout of TestScenario Users (actors)

[](#login-and-logout-of-testscenario-users-actors)

Two methods, which are built into your TestContext and TestScenario are `login($actor)` and `logout()`. These allow you to easily log in an actor (User) you created on your TestScenario, by providing either the User model instance or its TestContext key.

For example, to log in the `reader` user from the above examples, you can do something like this:

```
$scenario->login($scenario->reader);
//OR
$scenario->login('reader');
```

### Handling TestContext models and data

[](#handling-testcontext-models-and-data)

#### set(?string $key, mixed $value)

[](#setstring-key-mixed-value)

This method assigns a value (usually a model instance) to the TestContext, with the given key.

You can create and assign 'anonymous' models, by providing `null` for the key. This will assign the model to a randomly generated key.

#### get(string $key)

[](#getstring-key)

This method retrieves the TestContext value assigned to the given key, or null if no value was assigned to that key.

#### getInstance(string|iterable $expectedClass, mixed $keyOrInstance, bool $required = false)

[](#getinstancestringiterable-expectedclass-mixed-keyorinstance-bool-required--false)

This method can be used whenever you want to get an object of a specific class, but you want to be able to accept either the model or its context key.

If the model was given as the second argument ($attributeOrInstance) it is just returned if its class matches the expected class. If a context key was given (as a string), the object from the context is retrieved and returned if its class matches the expected class.

In some cases, you might allow the model to have one of many classes, so you can provide a list of acceptable classes as the first argument. This is an edge case, but there were some valid use cases where this was handy.

#### refreshModels()

[](#refreshmodels)

This method refreshes all model instances, by re-fetching them from the DB (using their `refresh()` method). In some cases, this is needed, because applications are stateless and your real life application uses fresh models in subsequent requests.

Provisioning databases for E2E tests
------------------------------------

[](#provisioning-databases-for-e2e-tests)

You can now reuse your scenarios outside of PHPUnit / Pest to seed state for Playwright, Cypress or any other end-to-end harness.

1. **Publish the config** (optional, only if you want custom aliases):

    ```
    php artisan vendor:publish --tag=test-scenarios-config
    ```

    Map short aliases to your scenario classes inside `config/test-scenarios.php`.
2. **Provision via artisan**

    ```
    php artisan test-scenarios:provision blog \
        --data='{"locale":"en"}' \
        --connection=sqlite
    ```

    Options:

    - `--data=` accepts raw JSON or `@path/to/file.json`.
    - The command always runs `migrate:fresh`; data is deterministic, so no JSON output is emitted.
    - `--seed` runs `Database\Seeders\DatabaseSeeder` after `migrate:fresh`. By default no seeders are executed; scenarios should provision all data.
    - `--connection=` temporarily overrides `database.default`.
    - `--refresh-models` / `--no-refresh-models` override the config default.
    - Commands refuse to run when `APP_ENV=production` to avoid mutating live databases.
3. **Provision from Playwright** using the bundled helper located at `vendor/antonioprimera/laravel-test-scenarios/playwright/index.js`:

    ```
    const { provisionScenario } = require(
      '../../vendor/antonioprimera/laravel-test-scenarios/playwright'
    );

    module.exports = async () => {
      await provisionScenario('blog', {
        data: { locale: 'en' },
        artisanPath: '../artisan',
      });

      process.env.E2E_ADMIN_EMAIL = 'admin@bforum.test';
    };
    ```

For a deeper dive (including architecture notes) check `docs/e2e-provisioning.md`.

### Agent tooling &amp; docs

[](#agent-tooling--docs)

- `php artisan test-scenarios:docs [--section=agents|readme]` – print the package README and the agent-specific playbook (`docs/agents.md`).
- `php artisan test-scenarios:list [--json]` – show every registered scenario alias, its class, and the docblock summary. Keep the docblocks up to date so other agents know what data exists.
- `php artisan test-contexts:list [--json]` – list the registered `TestContext` classes along with their public builder methods (requires you to register contexts inside `config/test-scenarios.php`).

Agents should always follow the deterministic-data guidelines laid out in `docs/agents.md` before attempting to extend or consume scenarios.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance60

Regular maintenance activity

Popularity15

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity70

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~163 days

Recently: every ~178 days

Total

9

Last Release

251d ago

Major Versions

v1.6.0 → v2.0.02025-09-10

PHP version history (3 changes)v1.4PHP 7.4|^8.0

v1.6.0PHP ^8.0

v2.0.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/77cac31fc31444fb45ef19e3628a7b243b5456679a9e6db635aa3b979bfdbefc?d=identicon)[AntonioPrimera](/maintainers/AntonioPrimera)

---

Top Contributors

[![AntonioPrimera](https://avatars.githubusercontent.com/u/23128666?v=4)](https://github.com/AntonioPrimera "AntonioPrimera (5 commits)")

### Embed Badge

![Health badge](/badges/antonioprimera-laravel-test-scenarios/health.svg)

```
[![Health](https://phpackages.com/badges/antonioprimera-laravel-test-scenarios/health.svg)](https://phpackages.com/packages/antonioprimera-laravel-test-scenarios)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.5k55.4M8.5k](/packages/larastan-larastan)[illuminate/testing

The Illuminate Testing package.

3316.5M139](/packages/illuminate-testing)[calebdw/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

15118.7k4](/packages/calebdw-larastan)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
