Небольшие хитрости для тестирования веб-приложений на Laravel с использованием Model Factories

от автора

Введение

Давайте представим, что мы разрабатываем небольшое веб-приложение на Laravel версии выше 6 и хотим написать для него тесты.

Содержание статьи приведено ниже:

  1. Описание предметной области
  2. Создание приложения
  3. Создание сущностей
  4. Написание тестов
  5. Проблема
  6. Решение

Описание предметной области

Мы будем разрабатывать интернет-магазин, в котором некие пользователи могут сделать некий заказ. Из вышеперечисленного получаем, что основными сущностями предметной области будут пользователь, заказ и товары. Между пользователем и заказом связь один-ко-многим, т. е. у пользователя может быть много заказов, а у заказа — только один пользователь (для заказа наличие пользователя обязательно). Между заказом и товарами связь многие-ко-многим, т. к. товар может быть в разных заказах и заказ может состоять из многих товаров. Для упрощения опустим товары и сосредоточимся только на пользователях и заказах.

Создание приложения

Приложения на Laravel очень просто создавать, используя пакет-создатель приложений. После его установки создание нового приложения умещается в одну команду:

laravel new shop

Создание сущностей

Как было сказано выше, нам нужно создать две сущности — пользователя и заказ. Так как Laravel поставляется с готовой сущностью пользователя, то перейдём к процессу создания модели заказа. Нам понадобится модель заказа, миграция для БД и фабрику для создания экземпляров. Команда для создания всего этого:

php artisan make:model Order -m -f

После выполнения команды мы получим файл модели в App/, файл миграции в папке database/migrations/ и фабрику в database/factories/.

Перейдём к написанию миграции. В заказе можно хранить много информации, но мы обойдёмся привязкой к пользователю с внешним ключом и таймстемпами. Должно получиться что-то такое:

<?php  use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;  class CreateOrdersTable extends Migration {     /**      * Run the migrations.      *      * @return void      */     public function up()     {         Schema::create('orders', function (Blueprint $table) {             $table->id();             $table->unsignedBigInteger('user_id');             $table->timestamps();              $table->foreign('user_id')->references('id')->on('users')                 ->onDelete('cascade');         });     }      /**      * Reverse the migrations.      *      * @return void      */     public function down()     {         Schema::dropIfExists('orders');     } }

Теперь к модели. Заполним свойство fillable и сделаем релейшен к пользователю:

<?php  namespace App;  use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo;  class Order extends Model {     protected $fillable = ['user_id'];      /**      * Relation to user      * @return BelongsTo      */     public function user(): BelongsTo     {         return $this->belongsTo(User::class);     } }

Переходим к фабрике. Помним, что связь с пользователем является обязательной, поэтому при запуске фабрики будем запускать фабрику пользователя и брать её id.

<?php  /** @var \Illuminate\Database\Eloquent\Factory $factory */  use App\Order; use App\User; use Faker\Generator as Faker;  $factory->define(Order::class, function (Faker $faker) {     return [         'user_id' => factory(User::class)->create()->id     ]; });

Сущности готовы, переходим к написанию тестов.

Написание тестов

По стандарту, Laravel использует PHPUnit для тестирования. Создать тесты для заказа:

php artisan make:test OrderTest

Файл теста можно найти в tests/Feature/. Для обновления состояния БД перед запуском тестов будем использовать трейт RefreshDatabase.

Тест №1. Проверим работу фабрики

<?php  namespace Tests\Feature;  use App\Order; use App\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase;  class OrderTest extends TestCase {     use RefreshDatabase;      /** @test */     public function order_factory_can_create_order()     {         // When we use Order factory         $order = factory(Order::class)->create();         // Then we should have new Order::class instance         $this->assertInstanceOf(Order::class, $order);     } }

Тест прошел!

Тест №2. Проверим наличие пользователя у заказа и работу релейшена

/** @test */ public function order_should_have_user_relation() {     // When we use Order factory     $order = factory(Order::class)->create();     // Then we should have new Order::class instance with user relation     $this->assertNotEmpty($order->user_id);     $this->assertInstanceOf(User::class, $order->user); }

Тест прошел!

Тест №3. Проверим, что мы можем передать своего пользователя в фабрику

/** @test */ public function we_can_provide_user_id_to_order_factory() {     // Given user     $user = factory(User::class)->create();     // When we use Order factory and provide user_id parameter     $order = factory(Order::class)->create(['user_id' => $user->id]);     // Then we should have new Order::class instance with provided user_id     $this->assertEquals($user->id, $order->user_id); }

Тест прошёл!

А теперь представьте, что вам важен факт того, что при создании одного заказа на одного пользователя в системе создавался только один пользователь. Такая проверка может показаться высосанной из пальца, но есть ситуации, когда соблюдение количества сущностей важно.

Тест №4. Проверим, что в системе при создании одного заказа создаётся один пользователь

/** @test */ public function when_we_create_one_order_one_user_should_be_created() {     // Given user     $user = factory(User::class)->create();     // When we use Order factory and provide user_id parameter     $order = factory(Order::class)->create(['user_id' => $user->id]);     // Then we should have new Order::class instance with provided user_id     $this->assertEquals($user->id, $order->user_id);     // Let's check that system has one user in DB     $this->assertEquals(1, User::count()); }

Тест проваливается! Оказывается, в базе данных на момент запуска проверки уже было два пользователя. Как так? Разберёмся в следующем шаге.

Проблема

Если в фабрику были переданы значения, то фабрика выбирает то, что ей передали — третий тест как пример. Но также фабрика выполняет код, записанный в значении ключа пользователя, т. е. создаёт пользователя, несмотря на то, что мы передали своего. Давайте разбираться, что с этим можно делать.

Решение

Опишу решение, которым пользуюсь сам. В PHP есть функция, с помощью которой можно получить n-ый аргумент функции — func_get_arg(), ей мы воспользуемся для изменения поведения фабрики заказов. По стандарту, в фабрику первым (нулевым) аргументом передаётся Faker, а вторым аргументом — массив значений, переданных в метод create() фабрики заказа. Соответственно, чтобы получить список переданных значений, нужно взять второй (первый) аргумент функции. В назначения значений важных ключей фабрики передадим анонимную функцию, которая будет проверять, было ли передано значение по ключу или нет. Что имеем в итоге:

$factory->define(Order::class, function (Faker $faker) {     // Получаем массив переданных значений     $passedArguments = func_get_arg(1);     return [         'user_id' => function () use ($passedArguments) {             // Если не передали user_id, то создаём своего             if (! array_key_exists('user_id', $passedArguments)) {                 return factory(User::class)->create()->id;             }         }     ]; });

Запускаем тест №4 ещё раз — он проходит!

Вот и всё, чем я хотел поделиться. Часто встречаюсь с проблемой, что необходимо контролировать количество сущностей после какого-то действия внутри системы, и стандартная реализация фабрик с этим не особо справляется.

Буду рад слышать ваши хитрости, которыми вы пользуетесь при разработке на Laravel или PHP.

ссылка на оригинал статьи https://habr.com/ru/post/493926/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *