单元测试 Laravel FormRequest

Posted

技术标签:

【中文标题】单元测试 Laravel FormRequest【英文标题】:Unit Test Laravel's FormRequest 【发布时间】:2016-08-26 23:09:18 【问题描述】:

我正在尝试对各种自定义 FormRequest 输入进行单元测试。我找到了解决方案:

    建议使用$this->call(…) 方法并用预期值(link to answer) 断言response。这太过分了,因为它直接依赖于 RoutingControllers

    Taylor 的测试,来自 Laravel 框架 found in tests/Foundation/FoundationFormRequestTest.php。那里做了很多模拟和开销。

我正在寻找一种解决方案,我可以根据规则对单个字段输入进行单元测试(独立于同一请求中的其他字段)

示例表单请求:

public function rules()

    return [
        'first_name' => 'required|between:2,50|alpha',
        'last_name'  => 'required|between:2,50|alpha',
        'email'      => 'required|email|unique:users,email',
        'username'   => 'required|between:6,50|alpha_num|unique:users,username',
        'password'   => 'required|between:8,50|alpha_num|confirmed',
    ];

所需的测试:

public function testFirstNameField()

   // assertFalse, required
   // ...

   // assertTrue, required
   // ...

   // assertFalse, between
   // ...


public function testLastNameField()

    // ...

如何对每个字段的每个验证规则进行单独的单元测试(断言)

【问题讨论】:

嗯,正如你所说,FormRequests 在 Laravel 中进行了测试,所以你不必再次测试它们,验证也在 Laravel 中测试。我真的没有得到你想要测试的东西...... 所以是的,这就是我在第一条评论中想说的,验证规则已经在 Laravel 中测试过了。在您的示例中,您只想测试输入是否对您的规则集有效,但规则有效,这就是 Laravel 测试告诉我们的。我不确定是否有必要再次测试它。恕我直言,这与写 $this->assertEquals([ 'first_name' => 'required|between:2,50|alpha'....], (new FormRequest)->rules()) 相同 我希望根据设置为有效和无效输入的规则来测试输入 我对这个问题投了赞成票,并将所有其他类似问题重定向到这里。我希望它能帮助很多人。 @PeterPan666 当然,这适用于简单的规则。但是如果你结合规则并使用正则表达式等,那么事情就会变得更加复杂。我相信 Laravel 会根据正则表达式测试输入,但我不相信自己完全理解规则 API 并始终编写正确的正则表达式。有一种方法来测试它会很好。有人实际上为此写了一个包*,但我认为这应该是开箱即用的。 * github.com/mohammedmanssour/form-request-tester 【参考方案1】:

我在 Laracast 上找到了一个很好的解决方案,并添加了一些自定义。

守则

/**
 * Test first_name validation rules
 * 
 * @return void
 */
public function test_valid_first_name()

    $this->assertTrue($this->validateField('first_name', 'jon'));
    $this->assertTrue($this->validateField('first_name', 'jo'));
    $this->assertFalse($this->validateField('first_name', 'j'));
    $this->assertFalse($this->validateField('first_name', ''));
    $this->assertFalse($this->validateField('first_name', '1'));
    $this->assertFalse($this->validateField('first_name', 'jon1'));


/**
 * Check a field and value against validation rule
 * 
 * @param string $field
 * @param mixed $value
 * @return bool
 */
protected function validateField(string $field, $value): bool

    return $this->validator->make(
        [$field => $value],
        [$field => $this->rules[$field]]
    )->passes();


/**
 * Set up operations
 * 
 * @return void
 */
public function setUp(): void

    parent::setUp();

    $this->rules     = (new UserStoreRequest())->rules();
    $this->validator = $this->app['validator'];

更新

有一个e2e 方法可以解决同样的问题。您可以POST 将要检查的数据发送到相关路由,然后查看响应是否包含会话错误

$response = $this->json('POST', 
    '/route_in_question', 
    ['first_name' => 'S']
);
$response->assertSessionHasErrors(['first_name']);

【讨论】:

确认规则的密码呢? @ClaudioLudovicoPanetta:将validateField 中的$field 更改为要检查的数据数组。示例:protected function validateField(array $data, string $rule_to_check) return $this->validator->make($data, [$rule_to_check => $this->rules[$rule_to_check]]) ->passes(); 然后:$this->assertTrue($this->validateField(['password' => 'correcthorsebatterystaple', 'password_confirmation' => 'correcthorsebatterystaple'], 'password')); 感谢您回答自己的问题。你很体贴。 请注意,使用这种方法,数组验证不起作用。为此,您必须向验证器传递一个更大的数组,包括嵌套字段(如名称。*) @matiaslauriti 但这根本不是在测试框架。这是测试 FormRequest 是否使用了正确的规则。此测试中没有任何内容可以测试实现。它只说“对于应用于 first_name 的规则,表明‘j’是无效的”。使用 ->get 或 ->post 进行功能测试很好,但是当在许多路由中重用相同的 FormRequests 时,您只需重复验证和授权测试。测试 OP 的方式允许您单独测试 FormRequest,并在控制器测试中模拟它。【参考方案2】:

朋友们,请做好单元测试,毕竟你在这里测试的不仅仅是rulesvalidationDatawithValidator函数可能也在那里。

应该是这样的:

<?php

namespace Tests\Unit;

use App\Http\Requests\AddressesRequest;
use App\Models\Country;
use Faker\Factory as FakerFactory;
use Illuminate\Routing\Redirector;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
use function app;
use function str_random;

class AddressesRequestTest extends TestCase



    public function test_AddressesRequest_empty()
    
        try 
            //app(AddressesRequest::class);
            $request = new AddressesRequest([]);
            $request
                ->setContainer(app())
                ->setRedirector(app(Redirector::class))
                ->validateResolved();
         catch (ValidationException $ex) 

        
        //\Log::debug(print_r($ex->errors(), true));

        $this->assertTrue(isset($ex));
        $this->assertTrue(array_key_exists('the_address', $ex->errors()));
        $this->assertTrue(array_key_exists('the_address.billing', $ex->errors()));
    


    public function test_AddressesRequest_success_billing_only()
    
        $faker = FakerFactory::create();
        $param = [
            'the_address' => [
                'billing' => [
                    'zip'        => $faker->postcode,
                    'phone'      => $faker->phoneNumber,
                    'country_id' => $faker->numberBetween(1, Country::count()),
                    'state'      => $faker->state,
                    'state_code' => str_random(2),
                    'city'       => $faker->city,
                    'address'    => $faker->buildingNumber . ' ' . $faker->streetName,
                    'suite'      => $faker->secondaryAddress,
                ]
            ]
        ];
        try 
            //app(AddressesRequest::class);
            $request = new AddressesRequest($param);
            $request
                ->setContainer(app())
                ->setRedirector(app(Redirector::class))
                ->validateResolved();
         catch (ValidationException $ex) 

        

        $this->assertFalse(isset($ex));
    



【讨论】:

如果您想非常具体,那么您应该将整个数据集作为一个特征而不是作为一个单元进行测试。 @DovBenyominSohacheski,NO.单元测试是一种更快的选择,它可以让您获得更好的抽象和封装。这也是问题的一部分。你可以从我的帖子中学习,而不是练习你的报复性投票。考虑成为更好的开发人员,而不是你的自尊心,我们都在这里学习。 我不会在测试中使用随机数据。除此之外,这似乎是一个很好的解决方案。虽然我觉得 FormRequest 在设计时并没有考虑到可测试性。 @Luc ,我建议在一些测试中使用随机数据,因为有时它会给你一些你可能会忽略的东西。例如,多亏了随机数据,我发现美国邮政编码可能由 2 个数字组成,而另外一次,当我取一个名字的 substr 作为简短参考时,我没有修剪那个 substr,有些名字是有空间作为 substr 的最后一个字符,它正在阻止其他一些逻辑。但是,不要误会我的意思,在某些情况下,预定义的值是必须的。 永远不会测试框架/核心......这真的是错误的。您必须对您的代码进行单元测试,在这种情况下将进行feature 测试并查看它是否失败(预期与否),因此FormRequest 无论如何都会完全运行......我无法相信有人建议做好单元测试并建议测试框架...【参考方案3】:

我看到这个问题有很多观点和误解,所以我会添加我的一粒沙子来帮助任何仍然有疑问的人。

首先,请记住永远不要测试框架,如果您最终做了与其他答案类似的事情(构建或绑定框架核心的模拟(忽略 Facades),那么您在做与测试相关的错误)。

因此,如果您想测试控制器,始终的方法是:对其进行功能测试。永远不要对它进行单元测试,不仅对它进行单元测试很麻烦(使用数据创建请求,可能是特殊要求)而且还要实例化控制器(有时它不是new HomeController 并完成......)。

他们解决作者问题的方法是像这样进行特征测试(记住,是一个例子,有很多方法):

假设我们有这样的规则:

public function rules()

    return [
        'name' => ['required', 'min:3'],
        'username' => ['required', 'min:3', 'unique:users'],
    ];

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class HomeControllerTest extends TestCase

    use RefreshDatabase;

    /*
     * @dataProvider invalid_fields
     */
    public function test_fields_rules($field, $value, $error)
    
        // Create fake user already existing for 'unique' rule
        User::factory()->create(['username' => 'known_username']);

        $response = $this->post('/test', [$field => $value]);

        $response->assertSessionHasErrors([$field => $error]);
    

    public function invalid_fields()
    
        return [
            'Null name' => ['name', null, 'The name field is required.'],
            'Empty name' => ['name', '', 'The name field is required.'],
            'Short name' => ['name', 'ab', 'The name must be at least 3 characters.'],
            'Null username' => ['username', null, 'The username field is required.'],
            'Empty username' => ['username', '', 'The username field is required.'],
            'Short username' => ['username', 'ab', 'The username must be at least 3 characters.'],
            'Unique username' => ['username', 'known_username', 'The username has already been taken.'],
        ];
    

就是这样...这就是进行此类测试的方式...无需实例化/模拟和绑定任何框架(Illuminate 命名空间)类。

我也在利用 PHPUnit,我使用的是 data providers,所以我不需要复制粘贴测试或创建测试将调用的 protected/private 方法来“设置” " 任何东西...我重用测试,我只是更改输入(字段、值和预期错误)。

如果您需要测试是否正在显示view,只需执行$response-&gt;assertViewIs('whatever.your.view');,您还可以传递第二个属性(但使用assertViewHas)来测试视图中是否有变量(以及期望值)。同样,无需实例化/模拟任何核心类...

考虑到这只是一个简单的例子,它可以做得更好一点(避免复制粘贴一些错误消息)。


最后一件重要的事情:如果你对这类事情进行单元测试,那么,如果你改变了后面的完成方式,你将不得不改变你的单元测试(如果你有模拟/实例化的核心类)。例如,也许您现在正在使用FormRequest,但后来您切换到其他验证方法,例如直接使用Validator,或者对其他服务的API 调用,因此您甚至没有直接在代码中进行验证。如果您进行功能测试,则不必更改单元测试代码,因为它仍然会接收相同的输入并给出相同的输出,但如果是单元测试,那么您将更改它的工作方式。 ..这就是我要说的关于这件事的不可以的部分...

始终将测试视为:

    为它设置最小的东西(上下文)开始: 你的背景是什么,所以它有逻辑? 是否应该已经存在具有 X 用户名的用户? 我应该创建 3 个模型吗? 等 调用/执行您想要的代码: 将数据发送到您的 URL (POST/PUT/PATCH/DELETE) 访问 URL (GET) 执行您的 Artisan 命令 如果是单元测试,请实例化您的类,然后调用所需的方法。 断言结果: 根据您的预期断言数据库的更改 断言返回的值是否与您的预期/想要的匹配 断言文件是否以任何所需方式(删除、更新等)发生更改 断言您期望发生的任何事情

因此,您应该将测试视为一个黑匣子。输入 -> 输出,无需复制中间...您可以设置一些假货,但不能伪造所有内容或它的核心...您可以模拟它,但我希望您理解我的意思,此时……

【讨论】:

以上是关于单元测试 Laravel FormRequest的主要内容,如果未能解决你的问题,请参考以下文章

自定义 Laravel FormRequest 自动验证方法

Laravel 5.5 FormRequest 自定义错误消息

Laravel 6 Backpack 4.0:如何在 FormRequest 类中获取当前页面 ID,或者我可以在不使用 FormRequest 类的情况下获得吗?

Laravel:当我添加所需的规则时,FormRequest 验证失败

PHPUnit env中的Laravel自定义FormRequest

如何在 Laravel 5.5 的 FormRequest 类中返回自定义响应?