单元测试Laravel的FormRequest

Dov*_*ski 18 php unit-testing request laravel laravel-5.2

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

  1. 建议使用该$this->call(…)方法并response使用期望值断言(链接到答案).这是过度的,因为它直接依赖于路由控制器.

  2. 泰勒的测试,从Laravel框架 中发现的 tests/Foundation/FoundationFormRequestTest.php.那里有很多嘲弄和开销.

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

SampleRequest示例:

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',
    ];
}
Run Code Online (Sandbox Code Playgroud)

期望的测试:

public function testFirstNameField()
{
   // assertFalse, required
   // ...

   // assertTrue, required
   // ...

   // assertFalse, between
   // ...
}

public function testLastNameField()
{
    // ...
}
Run Code Online (Sandbox Code Playgroud)

我如何单独测试(断言)每个字段的每个验证规则?

Dov*_*ski 20

我在Laracast上找到了一个很好的解决方案,并添加了一些定制功能.

代码

public function setUp()
{
    parent::setUp();
    $this->rules     = (new UserStoreRequest())->rules();
    $this->validator = $this->app['validator'];
}

/** @test */
public function 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'));
}

protected function getFieldValidator($field, $value)
{
    return $this->validator->make(
        [$field => $value], 
        [$field => $this->rules[$field]]
    );
}

protected function validateField($field, $value)
{
    return $this->getFieldValidator($field, $value)->passes();
}
Run Code Online (Sandbox Code Playgroud)

更新

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

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

  • 请注意,使用此方法时,数组验证不起作用。为了实现这一点,您必须向验证器传递一个更大的数组,包括嵌套字段(如名称。*) (2认同)
  • @matiaslauriti 但这根本不是测试框架。这是测试 FormRequest 是否使用正确的规则。此测试中没有任何内容来测试实现。它只说“对于应用于名字的规则,表明‘j’无效”。使用 ->get 或 ->post 进行功能测试很好,但是在许多路由中重用相同的 FormRequest 时,您将简单地重复验证和授权测试。测试 OP 的方式允许您单独测试 FormRequest,并在控制器测试中模拟它。 (2认同)

mat*_*iti 12

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

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

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

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

假设我们有这样的规则:

public function rules()
{
    return [
        'name' => ['required', 'min:3'],
        'username' => ['required', 'min:3', 'unique:users'],
    ];
}
Run Code Online (Sandbox Code Playgroud)
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.'],
        ];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我也在利用 PHPUnit,我正在使用,data providers所以我不需要复制粘贴测试或创建一个受保护/私有方法,测试将调用该方法来“设置”任何内容...我重用测试,我只是更改输入(字段、值和预期错误)。

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

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


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

始终将测试视为:

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

因此,您应该将测试视为黑匣子。输入 -> 输出,不需要复制它的中间...你可以设置一些假货,但不能伪造所有内容或它的核心...你可以嘲笑它,但我希望你明白我的意思,在此刻...


Yev*_*yev 5

朋友们,请好好进行单元测试,毕竟这里不光是rules你在测试,validationDatawithValidator函数也可能在那里。

应该这样做:

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


}
Run Code Online (Sandbox Code Playgroud)