代码之家  ›  专栏  ›  技术社区  ›  Dov Benyomin Sohacheski

单元测试Laravel的FormRequest

  •  33
  • Dov Benyomin Sohacheski  · 技术社区  · 8 年前

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

    1. 建议使用 $this->call(…) 方法并断言 response 具有预期值 (link to answer) 这太过分了,因为它直接依赖于 路由 控制器 .

    2. 泰勒试验,来自 拉拉维勒框架 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()
    {
        // ...
    }
    

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

    3 回复  |  直到 6 年前
        1
  •  34
  •   Josh Bonnick Dov Benyomin Sohacheski    3 年前

    我找到了一个很好的解决方案 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公司 解决同一问题的方法。你可以 岗位 要检查到相关路由的数据,然后查看响应是否包含 会话错误 .

    $response = $this->json('POST', 
        '/route_in_question', 
        ['first_name' => 'S']
    );
    $response->assertSessionHasErrors(['first_name']);
    
        2
  •  5
  •   Yevgeniy Afanasyev    5 年前

    朋友们,请正确地进行单元测试,毕竟,这不仅仅是 rules 你在这里测试 validationData withValidator 功能也可能在那里。

    应该这样做:

    <?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));
        }
    
    
    }
    
        3
  •  4
  •   matiaslauriti    3 年前

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

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

    因此,如果要测试控制器 总是 方法是:进行功能测试。永远不要对它进行单元测试,不仅单元测试(用数据创建请求,可能是特殊要求)很麻烦,而且还要实例化控制器(有时不是 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 所以我不需要复制粘贴测试或创建 受保护/专用 测试将调用的方法来“设置”任何内容……我重用测试,只更改输入(字段、值和预期错误)。

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

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


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

    始终将测试视为:

    1. 为其设置最基本的内容(上下文):
      • 你的背景是什么,所以它有逻辑?
      • 用户名为X的用户是否已经存在?
      • 我应该创建3个模型吗?
    2. 调用/执行所需代码:
      • 将数据发送到您的URL(POST/PUT/PATCH/DELETE)
      • 访问URL(GET)
      • 执行Artisan命令
      • 如果它是一个单元测试,那么实例化您的类,并调用所需的方法。
    3. 断言结果:
      • 如果需要更改,请断言数据库
      • 断言返回的值是否与您期望/想要的值匹配
      • 声明文件是否以任何所需方式更改(删除、更新等)
      • 说出你期望发生的一切

    因此,您应该将测试视为一个黑盒。输入->输出,不需要复制它的中间部分…你可以设置一些假的,但不能伪造所有内容或它的核心…你可以模仿它,但我希望你理解我的意思,在这一点上。。。