antonioprimera/laravel-test-scenarios

一个通用的包,允许您创建和重用测试场景

v1.5 2024-01-10 22:17 UTC

This package is auto-updated.

Last update: 2024-09-10 23:39:44 UTC


README

此包允许您轻松地在项目中创建和重用测试场景,甚至可以在您的包中发布测试场景。

概述

测试场景允许您创建一组模型,这些模型类似于实际应用程序,您可以在多个测试中重用这些场景。您可以创建任意数量的测试场景,包含不同的模型集合和应用设置(例如,应用设置、配置等)。

例如,如果您为包含2个用户(编辑者、读者)和2篇文章(已发布文章和未发布文章)的博客项目创建了一个BlogScenario,您可以轻松地进行如下操作

/** @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);
}

安装

您可以通过composer将此包安装到您的项目中,作为开发依赖

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

概述和基本概念

为了创建和使用场景,您必须首先创建“测试上下文”。测试上下文实际上是一个高级模型工厂,它应该足够智能,能够创建模型并调整应用的设置和配置。通常情况下,一个测试上下文就足够用于一个应用。

创建测试上下文之后,您可以使用这个上下文创建任意数量的场景。在测试场景的设置方法中,您将使用测试上下文的方法来创建模型并调整配置和应用设置。

创建测试场景之后,您只需在测试中实例化它们,就可以使用场景内创建的模型,并且可以轻松地使用场景上下文创建更多模型。

创建场景

步骤1:为您的项目创建一个测试上下文

在您的 tests/Context 文件夹中创建一个TestContext类,通过继承 AntonioPrimera\TestScenarios\TestContext

步骤2:创建您的模型工厂方法

TestContext将具有各种公共方法,用作工厂,允许您创建模型。例如,一个用于博客项目的TestContext将具有如下方法

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);
}

您应该使您的这些方法能够使用模型实例或上下文中已创建的模型的关键字。

为了保持您的TestContext类整洁,您应该将这些方法分组到特性中,并将它们包含到您的TestContext类中。例如,对于上面的例子,您将创建两个特性 tests/Context/Traits/CreatesPosts.phptests/Context/Traits/CreatesComments.php

步骤3:创建您的测试场景

您可以通过扩展抽象类 AntonioPrimera\TestScenarios\TestScenario 并实现2个抽象方法 setupcreateTestContext 来在 tests/Scenarios 文件夹中创建一个测试场景。

createTestContext 必须返回TestContext实例,该实例将用于创建场景模型和数据。

setup 方法应使用先前实例化的上下文来创建场景的所有模型并将它们分配给相应的键(这样就可以轻松检索)。

以下是一个示例

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);
    }
}

使用场景

模型是在测试场景中通过TestContext创建的,并由TestContext处理。测试场景是TestContext的包装器,并通过魔法获取器、魔法设置器和魔法方法调用来将所有属性和方法转发到TestContext。

换句话说,测试场景暴露了所有创建的模型和TestContext方法。

实例化场景

为了使用场景,您必须通过将其构造函数提供当前测试实例来实例化它。

$scenario = new SimpleBlogScenario($this);

或者您可以在TestCase的设置方法中实例化它。

protected SimpleBlogScenario $scenario;

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

或者您可以在您的基类TestCase中使用特性,它将自动实例化任何继承的TestCase中任何类型场景属性,因此您只需声明场景属性即可。

以下是如何看起来您的基类TestCase类

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
    }
}

以下是如何看起来您的应用TestCase之一

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);
    }
}

从TestScenario访问模型

当您在TestScenario内部创建模型时,您可以针对场景类内部创建的每个模型创建一个属性,但您也可以使用TestContext set($key, $valueOrModel) 方法。这将为上下文中的键分配一个模型或一些数据,它可以在场景实例上轻松(神奇地)访问。

然后您可以通过在TestScenario设置方法中分配给它们的键神奇地访问您创建的模型。例如,如果您创建了一个管理员用户并将其分配给admin键,您可以通过$scenario->admin访问它。

以下是一个测试示例,继续上面的博客示例

/** @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);
}

从TestScenario访问TestContext方法

您在TestContext中创建的任何公共方法都可以在您的TestScenario实例上访问。为了从您的IDE获取代码检查支持,您还可以直接在场景上访问上下文实例。

$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

登录和登出TestScenario用户(演员)

两个内置于您的TestContext和TestScenario中的方法是 login($actor)logout()。这些方法允许您通过提供User模型实例或其TestContext键轻松登录您在TestScenario中创建的演员。

例如,要登录上面示例中的reader用户,您可以这样做

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

处理TestContext模型和数据

set(?string $key, mixed $value)

此方法使用给定的键将值(通常是模型实例)分配给TestContext。

您可以通过为键提供null来创建并分配“匿名”模型。这会将模型分配给随机生成的键。

get(string $key)

此方法检索分配给给定键的TestContext值,如果没有为该键分配值,则为null。

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

此方法可以在您想要获取特定类的对象但能够接受模型或其上下文键时使用。

如果模型作为第二个参数($attributeOrInstance)给出,并且其类与期望的类匹配,则直接返回它。如果给出了上下文键(作为字符串),则检索并返回上下文中的对象,如果其类与期望的类匹配。

在某些情况下,您可能允许模型具有许多类之一,因此您可以提供一组可接受的类作为第一个参数。这是一个边缘情况,但有一些有效的用例,其中这很有用。

refreshModels()

此方法通过重新从DB(使用它们的refresh()方法)检索所有模型实例来刷新所有模型实例。在某些情况下,这可能需要,因为应用程序是无状态的,并且您的真实生活应用程序在后续请求中使用新鲜模型。