Введение
Давайте представим, что мы разрабатываем небольшое веб-приложение на Laravel версии выше 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/
Добавить комментарий