soyhuce/laravel-testing

Laravel测试助手

2.7.0 2024-09-03 14:28 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status GitHub PHPStan Action Status Total Downloads

为您的Laravel测试提供额外工具

安装

您可以通过composer安装此包

composer require soyhuce/laravel-testing --dev

用法

Laravel断言

要使用Laravel特定断言,您需要将 \Soyhuce\Testing\Assertions\LaravelAssertions::class 特性添加到您的测试类中。

assertModelIs

如果模型等于给定的模型,则匹配。

/** @test */
public function myTest()
{
    $user1 = User::factory()->createOne();
    $user2 = User::find($user1->id);
    
    $this->assertIsModel($user1, $user2);
}

assertCollectionEquals

如果集合相等,则匹配。

$collection1 = new Collection(['1', '2', '3']);
$collection2 = new Collection(['1', '2', '3']);
$this->assertCollectionEquals($collection1, $collection2);

如果两个集合包含相同元素、按相同键索引且顺序相同,则认为这两个集合相等。

$this->assertCollectionEquals(new Collection([1, 2]), new Collection([1, 2, 3])); // fail
$this->assertCollectionEquals(new Collection([1, 2, 3]), new Collection([1, 2])); // fail
$this->assertCollectionEquals(new Collection([1, 2, 3]), new Collection([3, 1, 2])); // fail
$this->assertCollectionEquals(new Collection([1, 2, 3]), new Collection([3, 1, 2])); // fail
$this->assertCollectionEquals(new Collection([1, 2, 3]), new Collection([1, 2, "3"])); // fail
$this->assertCollectionEquals(new Collection(['a' => 1, 'b' => 2, 'c' => 3]), new Collection(['a' => 1, 'b' => 2])); // fail
$this->assertCollectionEquals(new Collection(['a' => 1, 'b' => 2, 'c' => 3]), new Collection(['a' => 1, 'b' => 2, 'c' => 4])); // fail
$this->assertCollectionEquals(new Collection(['a' => 1, 'b' => 2, 'c' => 3]), new Collection(['a' => 1, 'b' => 2, 'd' => 3])); // fail
$this->assertCollectionEquals(new Collection(['a' => 1, 'b' => 2, 'c' => 3]), new Collection(['a' => 1, 'c' => 3, 'b' => 2])); // fail
$this->assertCollectionEquals(new Collection(['a' => 1, 'b' => 2, 'c' => 3]), new Collection(['a' => 1, 'b' => 2, 3])); // fail

如果集合包含模型,则 assertCollectionEquals 将使用 assertIsModel 的模型比较。

$user1 = User::factory()->createOne();
$user2 = User::find($user1->id);
$this->assertCollectionEquals(collect([$user1]), collect([$user2])); // Success

您可以在 assertCollectionEquals$expected 参数中提供一个数组

/** @test */
public function theUsersAreOrdered(): void
{
    $user1 = User::factory()->createOne();
    $user2 = User::factory()->createOne();
    
    $this->assertCollectionEquals(
        [$user1, $user2],
        User::query()->orderByDesc('id')->get()
    );
} 

TestResponse断言

所有这些方法都在 Illuminate\Testing\TestResponse 中可用

契约测试

需要 hotmeteor/spectator

  • TestResponse::assertValidContract(int $status) : 验证请求和响应根据契约有效。

数据

  • TestResponse::assertData($expect) : assertJsonPath('data', $expect) 的别名
  • TestResponse::assertDataPath(string $path, $expect) : assertJsonPath('data.'.$path, $expect) 的别名
  • TestResponse::assertDataPaths(array $expectations) : 对数组中的每个 $path => $expect 对运行 assertDataPath
  • TestResponse::assertDataMissing($item) : assertJsonMissingPath('data', $item) 的别名
  • TestResponse::assertDataPathMissing(string $path, $item) : assertJsonMissingPath('data.'.$path, $item) 的别名

Json

  • TestResponse::assertJsonPathMissing(string $path, $item) : 验证Json路径不包含 $item
  • TestResponse::assertJsonMessage(string $message) : assertJsonPath('message', $message) 的别名
  • TestResponse::assertSimplePaginated() : 验证响应是一个简单的分页响应。
  • TestResponse::assertPaginated() : 验证响应是一个分页响应。

视图

  • TestResponse::assertViewHasNull(string $key) : 验证键存在于视图中但为null。

独立的FormRequest测试

由于 TestsFormRequests 特性,可以独立测试FormRequests。

$testFormRequest = $this->createRequest(CreateUserRequest::class);

$testFormRequest 有一些方法来检查请求的授权和验证。

  • TestFormRequest::by(Authenticable $user, ?string $guard = null) : 在请求中设置已认证用户
  • TestFormRequest::withParams(array $params) : 设置路由参数
  • TestFormRequest::withParam(string $param, mixed $value) : 设置一个路由参数
  • TestFormRequest::validate(array $data): TestValidationResult : 获取验证结果
  • TestFormRequest::assertAuthorized() : 断言请求已授权
  • TestFormRequest::assertUnauthorized() : 断言请求未授权
  • TestValidationResult::assertPasses() : 断言验证通过
  • TestValidationResult::assertFails(array $errors = []) : 断言验证失败
  • TestValidationResult::assertValidated(array $expected) : 断言通过验证的属性和值是预期的

例如

$this->createRequest(CreateUserRequest::class)
    ->validate([
        'name' => 'John Doe',
        'email' => 'john.doe@email.com',
    ])
    ->assertPasses();

$this->createRequest(CreateUserRequest::class)
    ->validate([
        'name' => null,
        'email' => 12,
    ])
    //->assertFails() We can check that the validation fails without defining the fields nor error messages
    ->assertFails([
        'name' => 'The name field is required.',
        'email' => [
            'The email must be a string.',
            'The email must be a valid email address.',
        ]
    ]);

$this->createRequest(CreateUserRequest::class)
    ->by($admin)
    ->assertAuthorized();

$this->createRequest(CreateUserRequest::class)
    ->by($user)
    ->assertUnauthorized();

$this->createRequest(UpdateUserRequest::class)
    ->withArg('user', $user)
    ->validate([
        'email' => 'foo@email.com'
    ])
    ->assertPasses();

独立的JsonResource测试

由于 TestsJsonResources 特性,可以独立测试 JsonResources

TestsJsonResources::createResponse(JsonResource $resource, ?Request $request = null) 返回一个 Illuminate\Testing\TestResponse

$this->createResponse(UserResource::make($user))
    ->assertData([
        'id' => $user->id,
        'name' => $user->name,
        'email' => $user->email,
    ]);

匹配器

让我们看这个测试

$user = User::factory()->createOne();

$this->mock(DeleteUser::class)
    ->shouldReceive('execute')
    ->withArgs(function(User $executed) use ($user) {
        $this->assertIsModel($user, $executed);
        
        return true;
    })
    ->once();

// run some code wich will execute the mock

我们可以通过使用 Matcher 来简化这个测试。

$this->mock(DeleteUser::class)
    ->shouldReceive('execute')
    ->withArgs(Matcher::isModel($user))
    ->once();

对于集合,我们可以使用 Matcher::collectionEquals()

对于更复杂的情况,我们可以使用 Matcher::make

$user = User::factory()->createOne();
$roles = Role::factory(2)->create();

$this->mock(UpdateUser::class)
    ->shouldReceive('execute')
    ->withArgs(function(User $executed, string $email, Collection $executedRoles) use ($user, $roles) {
        $this->assertIsModel($user, $executed);
        $this->assertSame('foo@email.com', $email);
        $this->assertCollectionEquals($roles, $executedRoles);
        return true;
    })
    ->once();

// Refactored to
$this->mock(UpdateUser::class)
    ->shouldReceive('execute')
    ->withArgs(Matcher::make(
        $user,
        'foo@email.com',
        $roles
    ))
    ->once();

部分匹配

在某些情况下,我们只想检查几个参数或调用参数方法

$this->mock(CreateUser::class)
    ->shouldReceive('execute')
    ->withArgs(function(UserDTO $data, Collection $executedRoles) use ($team, $roles) {
        $this->assertSame('foo@email.com', $data->email);
        $this->assertSame('password', $data->password);
        $this->assertIsModel($team, $data->team())
        $this->assertCollectionEquals($roles, $executedRoles);
        return true;
    })
    ->once();

我们可以使用 Matcher::match 来定义对 $data 的断言

$this->mock(CreateUser::class)
    ->shouldReceive('execute')
    ->withArgs(Matcher::make(
        Matcher::match('foo@email.com', fn(UserDTO $data) => $data->email)
            ->match('password', fn(UserDTO $data) => $data->password)
            ->match($team, fn(UserDTO $data) => $data->team()),
        $roles
    ))
    ->once();

在特定对象属性的情况下,我们可以使用命名参数

$this->mock(CreateUser::class)
    ->shouldReceive('execute')
    ->withArgs(Matcher::make(
        Matcher::match(email: 'foo@email.com', password: 'password')->match($team, fn(UserDTO $data) => $data->team()),
        $roles
    ))
    ->once();

我们还可以检查对象类型

$this->mock(CreateUser::class)
    ->shouldReceive('execute')
    ->withArgs(Matcher::make(
        Matcher::of(UserDTO::class)->properties(email: 'foo@email.com', password: 'password'),
        $roles
    ))
    ->once();

ActionMock

特质 MocksActions 提供了一个 mockAction 方法来简单地模拟一个动作。按照惯例,一个动作是一个具有 execute 方法的类。

底层使用 Mockery::mock

它允许您轻松地定义对动作的期望。而不是

$user = User::factory()->createOne();

$this->mock(DeleteUser::class)
    ->shouldReceive('execute')
    ->withArgs(function(User $executed) use ($user) {
        $this->assertIsModel($user, $executed);
        
        return true;
    })
    ->once();

您可以编写

$user = User::factory()->createOne();

$this->mockAction(DeleteUser::class)
   ->with($user);

您还可以定义返回值并捕获它以在测试中使用。

$this->mockAction(CreateUser::class)
    ->with(new UserData(email: 'john.doe@email.com', password: 'password'))
    ->returns(fn() => User::factory()->createOne())
    ->in($user);

$this->postJson('register', ['email' => 'john.doe@email.com', 'password' => 'password'])
    ->assertCreated()
    ->assertJson([
        'id' => $user->id,
        'name' => $user->name,
        'email' => $user->email,
    ]);

辅助函数

在例如在模拟的 returnUsing 中,捕获回调的返回值可能是必要的。

$this->mock(CreateOrUpdateVersion::class)
    ->expects('execute')
    ->andReturnUsing(
        fn () => Version::factory()->for($package)->createOne()
    )
    ->once();

// I need created Version ! How do I do ?

在这种情况下,我们将使用 capture 函数

$this->mock(CreateOrUpdateVersion::class)
    ->expects('execute')
    ->andReturnUsing(capture(
        $version,
        fn () => Version::factory()->for($package)->createOne()
    ))    
    ->once();

一旦模拟执行,$version 就会被创建,并将包含回调的返回值。

测试

composer test

更新日志

有关最近更改的更多信息,请参阅 更新日志

贡献

有关详细信息,请参阅 贡献指南

安全漏洞

请审查我们的安全策略以了解如何报告安全漏洞: 安全策略

鸣谢

许可

MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件