Другой способ тестирования запросов формы Laravel
Многие разработчики борются с эффективностью тестирования запросов форм. В большинстве случаев вам придется писать отдельный модульный тест для каждого правила, определенного в вашем запросе формы. Это приводит к появлению множества тестов, таких как test_request_without_title и test_request_without_content. Все эти методы реализованы точно так же, только вызов вашей конечной точки с некоторыми разными данными. Это приведет к дублированию кода. В этом руководстве я покажу вам другой способ тестирования запроса формы, который, на мой взгляд, более понятен и улучшает ремонтопригодность ваших тестов.
Создание запроса формы
В этом примере я буду делать запрос формы для сохранения продукта.
php artisan make:request SaveProductRequest
Созданный класс файла будет помещен в App / Http / Requests.
Мы объявим набор правил проверки для этого запроса формы:
- Параметр title должен быть строкой длиной не более 50 символов.
- Параметр price должен быть числовым.
На данный момент это единственные два правила проверки.
Вот как выглядит класс SaveProductRequest :
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class SaveProductRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'title' => 'required|string|max:50', 'price' => 'required|numeric', ]; } }
В методе authorize вы можете проверить, есть ли у пользователя разрешение на выполнение этого запроса. Например, вы можете проверить, является ли пользователь администратором, но на данный момент любой может выполнить этот запрос.
Настройка модели
Давайте создадим модель продукта:
php artisan make:model Models/Product -m
Файл миграции выглядит так:
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateProductsTable extends Migration { public function up() { Schema::create('products', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title'); $table->double('price'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('products'); } }
Настройка контроллера и маршрута
Давайте настроим ProductController:
php artisan make:controller ProductController
И дайте ему очень простую реализацию:
<?php namespace App\Http\Controllers; use App\Http\Requests\SaveProductRequest; use App\Http\Resources\Product as ProductResource; use App\Models\Product; class ProductController extends Controller { public function store(SaveProductRequest $request) { $product = Product::create($request->validated()); return ProductResource::make($product); } }
Примечание.
ProductResource - это ресурс, который можно создать с помощью:
php artisan make:resource Product
И, наконец, добавьте маршрут в свой routes / api.php:
Route::post('/products', 'ProductController@store')->name('products.store');
Написание тестов
Прежде чем мы сможем начать наши тесты, мы должны создать тестовый файл:
php artisan make:test App/Http/Requests/SaveProductRequestTest
Примечание.
Я предпочитаю структурировать свои тесты таким образом, но вы можете не указывать папку App / Http / Requests.
Типичный набор тестов для этого контроллера может выглядеть так:
<?php namespace Tests\Feature\App\Http\Requests; use App\User; use Illuminate\Http\Response; use Tests\TestCase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\RefreshDatabase; class SaveProductRequestTest extends TestCase { use RefreshDatabase, WithFaker; protected function setUp(): void { parent::setUp(); $this->user = factory(User::class)->create(); } /** @test */ public function request_should_fail_when_no_title_is_provided() { $response = $this->actingAs($this->user) ->postJson(route('products.store'), [ 'price' => $this->faker->numberBetween(1, 50) ]); $response>assertStatus( Response::HTTP_UNPROCESSABLE_ENTITY ); $response->assertJsonValidationErrors('title'); } /** @test */ public function request_should_fail_when_no_price_is_provided() { $response = $this->actingAs($this->user) ->postJson(route('products.store'), [ 'title' => $this->faker->word() ]); $response->assertStatus( Response::HTTP_UNPROCESSABLE_ENTITY ); $response->assertJsonValidationErrors('price'); } /** @test */ public function request_should_fail_when_title_has_more_than_50_characters() { $response = $this->actingAs($this->user) ->postJson(route('products.store'), [ 'title' => $this->faker->paragraph() ]); $response->assertStatus( Response::HTTP_UNPROCESSABLE_ENTITY ); $response->assertJsonValidationErrors('price'); } /** @test */ public function request_should_pass_when_data_is_provided() { $response = $this->actingAs($this->user) ->postJson(route('products.store'), [ 'title' => $this->faker->word(), 'price' => $this->faker->numberBetween(1, 50) ]); $response->assertStatus(Response::HTTP_CREATED); $response->assertJsonMissingValidationErrors([ 'title', 'price' ]); } }
Именно так большинство разработчиков тестируют запрос формы. Это работает, и все тесты проходят успешно, но есть много повторяющегося кода. Единственное, что различается между тестами, - это данные, которые отправляются в конечную точку. Это можно сделать более эффективно.
Познакомьтесь с поставщиком данных PHPUnit
Поставщик данных PHPUnit предоставляет элегантный способ написания тестов для запросов формы Laravel. Поставщик данных позволяет один раз структурировать тесты и запускать их несколько раз с разными наборами данных.
Метод поставщика данных должен быть общедоступным и возвращать массив или объект, реализующий интерфейс Iterator. Вы можете указать поставщика данных с помощью аннотации @dataProvider.
Самый простой пример поставщика данных выглядит так:
/** * @dataProvider provider */ public function testAdd($a, $b, $c) { $this->assertEquals($c, $a + $b); } public function provider() { return [ [0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3] ]; }
Для каждого массива в методе provider будет вызываться метод testAdd. Аргументы, передаваемые методу testAdd, указываются в массиве от поставщика. Таким образом, первый вызов будет testAdd (0, 0, 0), а второй вызов - testAdd (0, 1, 1).
Как мы можем использовать это для проверки нашего запроса формы?
Точно так же, как мы указали числа для метода testAdd в поставщике данных, мы также можем указать данные, с которыми вызывается наша конечная точка. Затем мы запускаем каждый из этих массивов данных через класс Laravel Validator, чтобы проверить, проходят ли правила проверки.
Здесь важнее всего структура поставщика данных. В ключе массива провайдера данных указываем название теста. В этом массиве у нас есть два атрибута: переданные и данные. Атрибут переданный является логическим с ожидаемым результатом валидатора. Атрибут data содержит данные, которые мы хотим отправить в конечную точку.
Вот как будет выглядеть код:
<?php namespace Tests\Feature\App\Http\Requests; use App\Http\Requests\SaveProductRequest; use Faker\Factory; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class SaveProductRequestTest extends TestCase { use RefreshDatabase; /** @var \App\Http\Requests\SaveProductRequest */ private $rules; /** @var \Illuminate\Validation\Validator */ private $validator; public function setUp(): void { parent::setUp(); $this->validator = app()->get('validator'); $this->rules = (new SaveProductRequest())->rules(); } public function validationProvider() { /* WithFaker trait doesn't work in the dataProvider */ $faker = Factory::create( Factory::DEFAULT_LOCALE); return [ 'request_should_fail_when_no_title_is_provided' => [ 'passed' => false, 'data' => [ 'price' => $faker->numberBetween(1, 50) ] ], 'request_should_fail_when_no_price_is_provided' => [ 'passed' => false, 'data' => [ 'title' => $faker->word() ] ], 'request_should_fail_when_title_has_more_than_50_characters' => [ 'passed' => false, 'data' => [ 'title' => $faker->paragraph() ] ], 'request_should_pass_when_data_is_provided' => [ 'passed' => true, 'data' => [ 'title' => $faker->word(), 'price' => $faker->numberBetween(1, 50) ] ] ]; } /** * @test * @dataProvider validationProvider * @param bool $shouldPass * @param array $mockedRequestData */ public function validation_results_as_expected($shouldPass, $mockedRequestData) { $this->assertEquals( $shouldPass, $this->validate($mockedRequestData) ); } protected function validate($mockedRequestData) { return $this->validator ->make($mockedRequestData, $this->rules) ->passes(); } }
И тесты все еще проходят
Результат тот же, все тесты проходят, но уменьшается дублирование и улучшается ремонтопригодность. Что вы думаете об этом способе тестирования запросов формы? Вы тестируете запросы формы по-другому? Пожалуйста, дайте мне знать в комментариях.
Если вам понравился этот пост или он помог вам протестировать ваш код, обязательно ознакомьтесь с другими моими постами. Не стесняйтесь оставлять комментарии, если у вас есть какие-либо отзывы, вопросы или вы хотите, чтобы я написал о другой теме, связанной с Laravel.