soyhuce / laravel-testing
Laravel测试助手
Requires
- php: ^8.2
- illuminate/database: ^10.0 || ^11.0
- illuminate/support: ^10.0 || ^11.0
- illuminate/testing: ^10.0 || ^11.0
- phpunit/phpunit: ^10.0
- spatie/invade: ^1.0 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.6
- larastan/larastan: ^2.9.2
- nunomaduro/collision: ^7.10 || ^8.0
- orchestra/testbench: ^8.0 || ^9.0
- pestphp/pest: ^2.24
- pestphp/pest-plugin-laravel: ^2.2
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0
- phpstan/phpstan-phpunit: ^1.0
- spatie/laravel-data: ^3.9 || ^4.0
README
为您的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
更新日志
有关最近更改的更多信息,请参阅 更新日志。
贡献
有关详细信息,请参阅 贡献指南。
安全漏洞
请审查我们的安全策略以了解如何报告安全漏洞: 安全策略。
鸣谢
- Colin DeCarlo 对 FormRequest 隔离测试的贡献
- Bastien Philippe
- 所有贡献者
许可
MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件。