我正在尝试对各种自定义FormRequest
输入进行单元测试。我找到了解决方案:
建议使用$this->call(…)
方法并使用期望值(link to answer)声明response
。这是过度的,因为它直接依赖于路由和控制器。
泰勒的测试,来自 Laravel框架 found in tests/Foundation/FoundationFormRequestTest.php
。那里有很多嘲弄和开销。
我正在寻找一个解决方案,我可以根据规则对单个字段输入进行单元测试(独立于同一请求中的其他字段)。
示例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()
{
// ...
}
如何单独测试(断言)每个字段的每个验证规则?
答案 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 调用,因此您甚至没有直接在您的代码。如果您进行功能测试,则不必更改单元测试代码,因为它仍将接收相同的输入并给出相同的输出,但是如果是单元测试,那么您将更改其工作方式。 .. 这就是我要说的绝对不可以的部分...
始终将测试视为:
因此,您应该将测试视为一个黑匣子。输入 -> 输出,无需复制中间部分...您可以设置一些假货,但不能伪造所有内容或其中的核心...您可以嘲笑它,但我希望您理解我的意思,此时...
答案 2 :(得分:-1)
朋友们,请正确进行单元测试,毕竟,不仅您在这里测试rules
,而且validationData
和withValidator
函数也可能存在。 / 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));
}
}