单元测试Laravel的FormRequest

时间:2016-05-02 08:28:39

标签: php laravel unit-testing request laravel-5.2

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

  1. 建议使用$this->call(…)方法并使用期望值(link to answer)声明response。这是过度的,因为它直接依赖于路由控制器

  2. 泰勒的测试,来自 Laravel框架 found in tests/Foundation/FoundationFormRequestTest.php。那里有很多嘲弄和开销。

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

    示例FormRequest:

    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()
    {
        // ...
    }
    

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

3 个答案:

答案 0 :(得分:19)

我在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();
}

<强>更新

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

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

答案 1 :(得分:0)

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

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

因此,如果您想测试控制器,总是的方法是:对其进行功能测试。永远不要对它进行单元测试,不仅单元测试很麻烦(创建一个带有数据的请求,可能是特殊要求),而且还要实例化控制器(有时它不是 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->assertViewIs('whatever.your.view');,你也可以传递第二个属性(但使用assertViewHas)来测试视图是否有其中的变量(和所需的值)。同样,无需实例化/模拟任何核心类...

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


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

始终将测试视为:

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

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

答案 2 :(得分:-1)

朋友们,请正确进行单元测试,毕竟,不仅您在这里测试rules,而且validationDatawithValidator函数也可能存在。 / p>

这是应该怎么做:

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


}