worksome / request-factories
在Laravel中测试表单请求,无需所有样板代码。
Requires
- php: ^8.2
- illuminate/contracts: ^10.0 || ^11.0
Requires (Dev)
- larastan/larastan: ^2.8
- nunomaduro/collision: ^7.0 || ^8.0
- orchestra/testbench: ^8.20 || ^9.0
- pestphp/pest: ^2.30
- pestphp/pest-plugin-laravel: ^2.2
- worksome/coding-style: ^2.7
- dev-main
- v3.3.0
- v3.2.0
- v3.1.0
- v3.0.0
- 2.x-dev
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.0
- v2.2.0
- v2.1.0
- v2.0.2
- v2.0.1
- v2.0.0
- v1.1.1
- v1.1.0
- v1.0.1
- v1.0.0
- v0.2.1
- v0.2.0
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- dev-dependabot/github_actions/dependabot/fetch-metadata-1.4.0
- dev-2.x_laravel_10
- dev-pest-2
- dev-macro_fakes
- dev-better_new
- dev-parallel
This package is auto-updated.
Last update: 2024-09-08 13:25:01 UTC
README
在Laravel中测试表单请求,无需所有样板代码。
💡 嘘。虽然我们的示例使用了Pest PHP,但这在PHPUnit中也同样有效。
看看下面的测试
it('can sign up a user with an international phone number', function () { $this->put('/users', [ 'phone' => '+375 154 767 1088', 'email' => 'foo@bar.com', 🙄 'name' => 'Luke Downing', 😛 'company' => 'Worksome', 😒 'bio' => 'Blah blah blah', 😫 'profile_picture' => UploadedFile::fake()->image('luke.png', 200, 200), 😭 'accepts_terms_and_conditions' => true, 🤬 ]); expect(User::latest()->first()->phone)->toBe('+375 154 767 1088'); });
哎呀。看,我们只想测试电话号码,但由于我们的路由FormRequest有验证规则,我们必须同时发送所有这些额外字段。这种方法有几个缺点
- 它使测试变得混乱。测试应该是简洁且易于阅读的。但这完全不是。
- 它使编写测试变得令人讨厌。你可能对每个路由都有多个测试。你每次编写的测试都需要重复使用所有这些字段。
- 它需要了解FormRequest。在能够编写通过测试之前,你需要了解这个表单中每个字段的作用。如果不了解,你可能会陷入试错循环,或者在创建测试时出现错误。
我们认为这种体验可以大大改进。看看
it('can sign up a user with an international phone number', function () { SignupRequest::fake(); $this->put('/users', ['phone' => '+375 154 767 1088']); expect(User::latest()->first()->phone)->toBe('+375 154 767 1088'); });
要酷得多。这一切都归功于Request Factories。让我们深入了解...
安装
您可以通过Composer将包作为开发依赖项安装
composer require --dev worksome/request-factories
使用方法
首先,让我们创建一个新的RequestFactory
。一个RequestFactory
通常与您的应用程序中的FormRequest
配合使用(请求工厂也可以与标准请求一起使用!)。您可以使用make:request-factory
Artisan命令创建一个RequestFactory
php artisan make:request-factory "App\Http\Requests\SignupRequest"
请注意,我们将SignupRequest
FQCN作为参数传递。这将创建一个新的请求工厂在tests/RequestFactories/SignupRequestFactory.php
。
您也可以将所需的请求工厂名称作为参数传递
php artisan make:request-factory SignupRequestFactory
虽然您可以随意命名请求工厂,但我们推荐两种默认值以获得无缝体验
- 将它们放在
tests/RequestFactories
中。Artisan命令会为您完成这项工作。 - 使用
Factory
后缀。所以SignupRequest
变成SignupRequestFactory
。
工厂基础
让我们看看我们新创建的SignupRequestFactory
。您会看到类似以下内容
namespace Tests\RequestFactories; use Worksome\RequestFactories\RequestFactory; class SignupRequestFactory extends RequestFactory { public function definition(): array { return [ // 'email' => $this->faker->email, ]; } }
如果您之前使用过Laravel的模型工厂,这看起来会很熟悉。这是因为基本概念是相同的:模型工厂旨在为Eloquent模型生成数据,请求工厂旨在为表单请求生成数据。
definition
方法应该返回一个数组,包含可用于提交表单的有效数据。让我们为我们的示例SignupRequestFactory
填充它
namespace Tests\RequestFactories; use Worksome\RequestFactories\RequestFactory; class SignupRequestFactory extends RequestFactory { public function definition(): array { return [ 'phone' => '01234567890', 'email' => 'foo@bar.com', 'name' => 'Luke Downing', 'company' => 'Worksome', 'bio' => $this->faker->words(300, true), 'accepts_terms_and_conditions' => true, ]; } public function files(): array { return [ 'profile_picture' => $this->file()->image('luke.png', 200, 200), ]; } }
请注意,我们有一个faker
属性,可以轻松生成假内容,例如为我们的个人简介生成一段段落,还有一个我们可以声明的files
方法,以将文件与其他请求数据分开。
测试中的使用
那么我们如何在测试中使用这个工厂呢?根据您喜欢的风格,有几个选项。
在工厂中使用 create
此方法与 Laravel 的模型工厂最为相似。create
方法返回一个数组,然后您可以将它作为数据传递给 put
、post
或其他任何请求测试方法。
it('can sign up a user with an international phone number', function () { $data = SignupRequest::factory()->create(['phone' => '+44 1234 567890']); $this->put('/users', $data)->assertValid(); });
在请求工厂中使用 fake
由于您通常在每次测试中只发送一个请求,因此我们支持使用 fake
在全局范围内注册您的工厂。如果您使用这种方法,请确保它是 工厂上最后调用的方法,并且您在向相关端点发送请求之前调用它。
it('can sign up a user with an international phone number', function () { SignupRequestFactory::new()->fake(); $this->put('/users')->assertValid(); });
在表单请求中使用 fake
如果您使用过 Laravel 模型工厂,您可能已经习惯了在 Eloquent 模型上调用 ::factory()
来获取新的工厂实例。请求工厂具有类似的功能。您不需要做任何事情来启用此功能;我们自动通过宏将 ::fake()
和 ::factory()
方法注册到所有 FormRequests 上!
您可以在测试中使用这些方法,而不是直接实例化请求工厂
it('can sign up a user with an international phone number', function () { // Using the factory method... SignupRequest::factory()->fake(); // ...or using the fake method SignupRequest::fake(); $this->put('/users')->assertValid(); });
Pest PHP 中的 fakeRequest
如果您使用 Pest,我们提供了一种更高阶的方法,您可以将它链到您的测试中
// You can provide the form request FQCN... it('can sign up a user with an international phone number', function () { $this->put('/users')->assertValid(); })->fakeRequest(SignupRequest::class); // Or the request factory FQCN... it('can sign up a user with an international phone number', function () { $this->put('/users')->assertValid(); })->fakeRequest(SignupRequestFactory::class); // Or even a closure that returns a request factory... it('can sign up a user with an international phone number', function () { $this->put('/users')->assertValid(); })->fakeRequest(fn () => SignupRequest::factory());
您甚至可以将工厂方法链接到 fakeRequest
方法的末尾
it('can sign up a user with an international phone number', function () { $this->put('/users')->assertValid(); }) ->fakeRequest(SignupRequest::class) ->state(['name' => 'Jane Bloggs']);
覆盖请求工厂数据
请注意请求工厂在向您的请求注入数据时的优先级顺序。
- 传递给
get
、post
、put
、patch
、delete
或类似方法的任何数据都将始终具有优先级。 - 使用
state
定义的或对工厂调用并改变状态的函数将排在第二位。 - 工厂中定义的
definition
和files
方法中的数据排在最后,仅填充请求中缺少的属性。
让我们通过一个示例来展示这个优先级顺序
it('can sign up a user with an international phone number', function () { SignupRequest::factory()->state(['name' => 'Oliver Nybroe', 'email' => 'oliver@worksome.com'])->fake(); $this->put('/users', ['email' => 'luke@worksome.com'])->assertValid(); });
在 SignupRequestFactory
中定义的默认电子邮件是 foo@bar.com
。默认名称是 Luke Downing
。由于我们在调用 fake
之前使用 state
方法覆盖了 name
属性,因此表单请求中使用的名称实际上是 Oliver Nybroe
,而不是 Luke Downing
。
然而,由于我们向 put
方法传递了 luke@worksome.com
作为数据,这将优先于 所有其他定义的数据,包括 foo@bar.com
和 oliver@worksome.com
。
工厂的力量
工厂真的很酷,因为它们允许我们为我们的特性测试创建一个领域特定语言。由于工厂是类,我们可以添加声明式方法,这些方法作为状态转换器。
// In our factory... class SignupRequestFactory extends RequestFactory { // After the definition... public function withOversizedProfilePicture(): static { return $this->state(['profile_picture' => $this->file()->image('profile.png', 2001, 2001)]) } } // In our test... it('does not allow profile pictures larger than 2000 pixels', function () { SignupRequest::factory()->withOversizedProfilePicture()->fake(); $this->put('/users')->assertInvalid(['profile_picture' => 'size']); });
您还可以在 state
方法中使用点符号来改变请求数据中的深层嵌套键。
it('requires a postcode with the first line of an address', function () { SignupRequest::factory()->state(['address.line_one' => '1 Test Street'])->fake(); $this->put('/users')->assertInvalid(['address.postcode' => 'required']); });
state
方法是您在工厂上添加或更改数据的得力助手。那么,如果您想从请求中省略一个属性怎么办?尝试 without
方法!
it('requires an email address', function () { SignupRequest::factory()->without('email')->fake(); $this->put('/users')->assertInvalid(['email' => 'required']); });
💡 您可以在
without
方法中使用点符号来取消深层嵌套键的设置
您还可以将数组传递给 without
来一次性取消多个属性的设置。
有时,您可能有一个属性,您希望它基于其他属性的值。在这种情况下,您可以将闭包作为属性值提供,该闭包接收所有其他参数的数组
class SignupRequestFactory extends RequestFactory { public function definition(): array { return [ 'name' => 'Luke Downing', 'company' => 'Worksome', 'email' => fn ($properties) => Str::of($properties['name']) ->replace(' ', '.') ->append("@{$properties['company']}.com") ->lower() ->__toString(), // luke.downing@worksome.com ]; } }
偶尔,您可能会注意到您的应用程序中多个请求共享一组类似的字段子集。例如,注册表单和支付表单可能都包含地址数组。而不是在您的工厂中重复这些字段,您可以在工厂内部嵌套工厂
class SignupRequestFactory extends RequestFactory { public function definition(): array { return [ 'name' => 'Luke Downing', 'company' => 'Worksome', 'address' => AddressRequestFactory::new(), ]; } }
现在,当创建 SignupRequestFactory
时,它将为您解析 AddressRequestFactory
并用 AddressRequestFactory
定义中包含的所有字段填充 address
属性。很酷,不是吗?
请求工厂也与模型工厂协同工作。想象一下,你想要将一个 User
ID 传递给表单请求,但你需要创建数据库中的用户才能这样做。在请求工厂定义中实例化 UserFactory
就这么简单。
class StoreMovieController extends RequestFactory { public function definition(): array { return [ 'name' => 'My Cool Movie' 'owner_id' => User::factory(), ]; } }
由于 UserFactory
是在编译时创建的,我们避免了在手动覆盖 owner_id
字段时意外地将模型持久化到测试数据库。
不使用表单请求使用工厂
你的应用中的每个控制器都不需要后置表单请求。幸运的是,我们也支持伪造通用请求。
it('lets a guest sign up to the newsletter', function () { NewsletterSignupFactory::new()->fake(); post('/newsletter', ['email' => 'foo@bar.com'])->assertRedirect('/thanks'); });
解决常见问题
我遇到了一个 CouldNotLocateRequestFactoryException
当在 FormRequest
上使用 ::fake()
或 ::factory()
方法时,我们会尝试自动定位相关的请求工厂。如果你的目录结构由于某种原因不匹配,这个异常将会被抛出。
很容易解决,只需在你的表单请求中添加一个 public static $factory
属性。
class SignupRequest extends FormRequest { public static $factory = SignupRequestFactory::class; }
我在单个测试中调用多个路由并想要伪造它们
没问题。只需在每个请求之前对相关的请求工厂调用 fake
。
it('allows a user to sign up and update their profile', function () { SignupRequest::fake(); post('/signup'); ProfileRequest::fake(); post('/profile')->assertValid(); });
我不想使用默认位置存储请求工厂
没问题。我们提供了一个配置文件,你可以发布它来编辑请求工厂的路径和命名空间。首先,发布配置文件。
php artisan vendor:publish --tag=request-factories
现在,在新建的 config/request-factories.php
中,修改 path
和 namespace
键以满足你的要求。
return [ 'path' => base_path('request_factories'), 'namespace' => 'App\\RequestFactories', ];
测试
我们自豪于全面的测试套件和严格的静态分析。你可以通过 composer 脚本运行所有的检查。
composer test
为了使贡献变得极其容易,我们还提供了一个 docker-compose 文件,它会启动一个带有所有必需依赖项的容器。假设你已经安装了 docker,只需运行
docker-compose run --rm composer install # Only needed the first time docker-compose run --rm composer test # Run tests and static analysis
变更日志
请参阅 变更日志 以获取最近更改的更多信息。
致谢
许可
MIT 许可证 (MIT)。请参阅 许可文件 以获取更多信息。