TL;DR: из этого туториала вы узнаете, как построить и протестировать различные конечные точки RESTful API, созданного с помощью Laravel. Наш подход к достижению этой цели будет включать::
- Настройка среды тестирования
- Написание тестов для конечных точек нашего приложения с ожиданием, что они не будут работать
- А также структурирование и построение нашего API для прохождения тестов
Мы будем делать это постепенно в хронологическом порядке для каждой определенной конечной точки API. Полный исходный код этого учебника можно найти здесь, на GitHub.
Как инженер-программист, который рассматривает возможность сделать небольшой шаг вперед в разработке (и вы должны это сделать), тестовая разработка, часто называемая TDD, является одним из лучших методов, которые вы можете добавить в свой арсенал. Для выразительного тестирования ваших приложений Laravel поставляется вместе с PHPUnit, тестовой платформой для PHP. Он содержит удобные вспомогательные методы, полезные для создания и запуска тестовых сценариев, специально написанных для различных блоков в вашем приложении.
Предпосылки
Разумное знание объектно-ориентированного программирования с PHP поможет вам получить максимум пользы от этого урока. На протяжении всего урока я сделаю все возможное, чтобы объяснить концепции Laravel, которые я поднимаю, и предоставить ссылки для дальнейших исследований. Пожалуйста, не стесняйтесь делать паузу и просматривать их, если вы заблудились в какой-то момент – я обещаю, что этот учебник все еще будет здесь, когда вы вернетесь.
Ваша среда разработки также должна удовлетворять требованиям к серверу, установленным Laravel , включая глобальную установку Composer.
Вы можете настроить новые проекты Laravel либо с помощью Composer, либо с помощью установщика Laravel.
В этом уроке мы будем использовать PHPUnit и Xdebug для тестирования и анализа покрытия. Пожалуйста, убедитесь, что у вас установлен Xdebug в вашей системе.
Важность тестирования
Важность тестирования сводится к двум вещам-гарантии и ремонтопригодности.
Интернет изобилует историями об ошибках программного обеспечения, которые обходятся организациям в миллионы долларов ущерба. Среди всех этих историй есть одна общая тема-неадекватное тестирование. Либо никаких тестов не проводилось, либо тесты недостаточно охватывали все потенциальные сценарии, с которыми столкнется приложение. Как разработчики, мы склонны моделировать наши приложения вокруг идеальных сценариев, но производственная среда-это что угодно, только не идеал. Крайне важно, чтобы наши тесты охватывали все возможные сценарии и чтобы наше приложение вело себя ожидаемым образом во всех из них.
Вопрос ремонтопригодности основывается на гарантии. Вы когда-нибудь смотрели на фрагмент кода и думали о его рефакторинге, но сдерживались, потому что боялись сломать приложение? Вместо этого вы дублируете его в другой функции, а затем используете его вместо этого. Вот так код начинает гноиться (по словам дяди Боба) и появляются ошибки. Критическое изменение реализуется в одном классе, но не в дублировании в другом классе. Приложение, по сути, становится бомбой замедленного действия, ожидающей взрыва. Кроме того, правильное тестирование делает такие процессы, как CI/CD, более надежными — что позволяет быстро выпускать обновления. Излишне говорить, что невозможно переоценить важность тестирования.
Разработка На Основе Тестов
Test Driven Development (TDD) – это стиль программирования, основанный на трех китах: тестировании, кодировании и проектировании (рефакторинге). TDD несколько аномален для обычного опыта разработчика в том смысле, что мы склонны писать код сначала перед нашими тестовыми случаями. Однако это портит качество наших тестовых случаев, поскольку наши тесты будут руководствоваться нашим кодом (который обрабатывает только идеальные условия).
Кроме того, при написании кода сначала мы рискуем написать больше кода, чем это необходимо для решения проблемы, которая, не будучи проверенной, вводит ошибки в наше приложение.
TDD по сути представляет собой цикл из 3 событий:
- Написание тестового случая. Поскольку кода для запуска нет, этот тест завершится неудачей и также будет называться Красной фазой TDD.
- Напишите код для прохождения теста (не больше и не меньше). Это известно как зеленая фаза TDD
- Рефакторинг кода. Это может быть сделано в форме устранения дубликатов или реструктуризации в соответствии с лучшими практиками (DRY, KISS, SOLID, чтобы назвать несколько). Это также называется синей фазой TDD.
Этот цикл продолжается на протяжении всего процесса разработки приложения и даже во время его обслуживания. Например, если вы хотите настроить функцию приложения, вы начинаете с настройки соответствующего тестового набора в соответствии с новыми спецификациями. Это приведет к провалу теста. Затем вы настраиваете код, чтобы пройти тест, и, наконец, рефакторинг, чтобы убедиться, что код все еще чист.
Излишне говорить, что для формирования этой привычки требуется время, особенно если вы не привыкли к тестированию. Однако эту привычку можно развить, написав тестовые случаи и используя отчеты о покрытии для точной настройки качества кода.
В этом уроке мы возьмем приложение с минимальным плохим покрытием и напишем тестовые примеры для улучшения качества кода.
Настройка демонстрационного проекта
В этом уроке мы будем работать с инвестиционным симулятором хедж-фондов. В нашем симуляторе есть несколько стратегий, доступных пользователю для инвестирования. У пользователя также есть кошелек, куда возвращается доход от инвестиций сдаются на хранение. Для каждой инвестиции доходность определяется на основе того, является ли инвестиция успешной или нет. Каждая инвестиционная стратегия имеет два мультипликатора-доходность и облегчение. Если инвестиция успешна, то доходность инвестиций – это сумма вложенных средств, умноженная на доходность стратегии. Если нет, то инвестированная сумма умножается на облегчение стратегии, чтобы получить отдачу от инвестиций.
Идите вперед и клонируйте приложение
git clone https://github.com/yemiwebby/laravel-phpunit-starter
cd laravel-phpunit-starter
Потратьте несколько минут, чтобы просмотреть проект и ознакомиться с ним. Из кейса проекта мы можем выделить четыре ресурса:
User
Wallet
Strategy
Investment
За исключением Wallet
ресурса, маршрут API доступен для создания, обновления, удаления и поиска одного ресурса. Конечная точка также предоставляется для получения всех ресурсов. Вы можете просмотреть маршруты, предоставляемые APIroutes/api.php
-интерфейсом .
Затем установите зависимости приложения с помощью следующей команды:
composer install
cp .env.example .env
После завершения процесса перейдите к созданию базы данных для вашего приложения и .env
соответствующим образом измените файл
DB_CONNECTION=YOUR_DB_CONNECTOR
DB_PORT=3306 # 3306 for MySQL
DB_DATABASE=YOUR_DATABASE_NAME
DB_USERNAME=YOUR_DATABASE_USERNAME
DB_PASSWORD=YOUR_DATABASE_PASSWORD
Замените YOUR_DATABASE_NAME
,YOUR_DATABASE_USERNAME
, и YOUR_DATABASE_PASSWORD
с соответствующими значениями для вашей базы данных.
Существует существующая схема базы данных и классы сеялок, созданные для демонстрационного приложения и расположенные в папках database/migrations
и database/seeders
соответственно. Выполните следующую команду, чтобы обновить созданную базу данных схемой и затем ввести в нее некоторые тестовые данные:
php artisan migrate --seed
Подавайте заявку и попробуйте сделать несколько запросов
php artisan serve
Тестирование
Для тестирования вы будете использовать PHPUnit. Все ваши тесты будут расположены в tests
каталоге приложения. Структура папок, которую мы возьмем, будет несколько соответствовать структуре приложения. Код для тестирования контроллеров будет находиться в tests/Controller
. Имена, присвоенные вашим тестовым файлам, также будут совпадать с именем тестируемого класса. Например, имя файла, содержащего тестовые случаи для нашего InvestmentController, таково InvestmentControllerTests
.
Наконец, все ваши тестовые функции будут иметь префикс test
. Например, если вы хотите написать тестовый случай, чтобы убедиться, что ваш InvestmentController правильно создает инвестиции, вы можете назвать эту функцию testInvestmentIsCreatedSuccessfully
. Имена функций в ваших тестовых случаях должны быть описательными, чтобы мы могли понять, к чему стремится тест. Это также помогает нам управлять нашим кодом (разбитым на различные тесты, Если имя функции становится слишком длинным).
Команда для запуска всех наших тестов показана ниже
vendor/bin/phpunit
Чтобы сделать наши тесты быстрее, мы будем использовать SQLite для нашей тестовой базы данных. Это уже было настроено в phpunit.xml
файле.
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
tests/TestCase.php
был изменен для заполнения базы данных при настройке тестов. Для генерации случайных данных мы будем использовать пакет Faker. Кроме того, был добавлен волшебный геттер, так что вы можете легко получить доступ к Faker, когда вам нужно генерировать случайные данные. Ваш tests/TestCase.php
должен выглядеть так:
<?php
namespace Tests;
use Exception;
use Faker\Factory;
use Faker\Generator;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;
abstract class TestCase extends BaseTestCase {
use CreatesApplication, DatabaseMigrations;
private Generator $faker;
public function setUp()
: void {
parent::setUp();
$this->faker = Factory::create();
Artisan::call('migrate:refresh');
}
public function __get($key) {
if ($key === 'faker')
return $this->faker;
throw new Exception('Unknown Key Requested');
}
}
Покрытие кода
Помимо выполнения тестов, мы также хотим отслеживать покрытие кода. Это позволяет узнать, какая часть написанного кода покрывается тестовыми случаями. PHPUnit также может помочь в этом. Чтобы просмотреть отчет о покрытии после выполнения теста, добавьте --coverage-*
аргумент. Например, чтобы просмотреть отчет о покрытии кода сразу после завершения тестов, используйте следующую команду:
vendor/bin/phpunit --coverage-text
Для этого проекта вы будете генерировать свои отчеты в формате HTML, используя --coverage-html
аргумент. Этот аргумент требует наличия каталога для записи отчета. Для нашего проекта это будет tests/coverage
каталог.
vendor/bin/phpunit --coverage-html tests/coverage
Для упрощения процесса тестирования в файл composer.json
был добавлен скрипт с именем test. Чтобы запустить тесты и создать отчет HTML, введите следующее:
composer test
Это создаст HTML-файл в tests/coverage
каталоге. Откройте tests/coverage/index.html
файл в браузере, чтобы просмотреть отчет о покрытии кода.
Примечание: убедитесь, что вы добавили
xdebug.mode=coverage
в свойphp.ini
файл.
Вы также можете перейти по ссылкам, чтобы увидеть более подробный отчет. Например, отчет о покрытии для вашей папки Controller
:
Как вы можете видеть, ни один из написанных нами методов не охвачен тестами. Это означает, что мы не можем предсказать поведение нашего приложения. В идеале мы хотим, чтобы наше покрытие кода было как можно ближе к 100%, чтобы быть уверенными, что каждая строка кода, которую мы написали, должным образом покрыта нашими тестовыми случаями. Как вы также увидите позже в этом уроке, получение покрытия 100% недостаточно для гарантии того, что наше приложение будет должным образом протестировано. Хотя это хорошее начало, мы должны быть уверены, что наши тестовые случаи охватывают все возможные варианты в нашем приложении.
Написание первых тестовых случаев
Для начала давайте напишем несколько тестов для UserController
. Для каждого маршрута в нашем контроллере мы проверим, что он возвращает правильный код ответа. Мы также проверим, чтобы убедиться, что наш контроллер возвращает правильные данные для каждого запроса.
В tests
каталоге создайте новую папку с именем Controllers
. Создайте файл с именем UserControllerTests.php
и добавьте следующее:
<?php
namespace Tests\Controllers;
use Illuminate\Http\Response;
use Tests\TestCase;
class UserControllerTests extends TestCase {
public function testIndexReturnsDataInValidFormat() {
$this->json('get', 'api/user')
->assertStatus(Response::HTTP_OK)
->assertJsonStructure(
[
'data' => [
'*' => [
'id',
'first_name',
'last_name',
'email',
'created_at',
'wallet' => [
'id',
'balance'
]
]
]
]
);
}
}
Эта функция будет делать GET
HTTP-запрос к index
функции UserController
. Сначала он проверяет, соответствует ли состояние ответа функции response HTTP_OK
(200
). Он также проверяет, соответствует ли структура ответа API той, что указана в функции. Вы используете '*' => […]
его, чтобы указать, что ожидаете, что этот узел будет повторен или пуст.
Затем добавьте тестовый случай, чтобы убедиться, что store
функция работает правильно. Добавьте в тестовый случай следующую функцию:
public function testUserIsCreatedSuccessfully() {
$payload = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
];
$this->json('post', 'api/user', $payload)
->assertStatus(Response::HTTP_CREATED)
->assertJsonStructure(
[
'data' => [
'id',
'first_name',
'last_name',
'email',
'created_at',
'wallet' => [
'id',
'balance'
]
]
]
);
$this->assertDatabaseHas('users', $payload);
}
Аналогично первому тесту, вы создали поддельные данные с помощью пакета Faker bundle и сделали запрос к store
функции UserController
. Вы гарантировали, что код ответа HTTP_CREATED
(201
) будет возвращен и проверен, чтобы убедиться, что он соответствует ожидаемой структуре ответа. Последнее утверждение должно гарантировать, что пользователь существует в базе данных.
В том же духе вы можете проверить свою функцию show
следующим образом:
// ...
// add your import statements to the top
use App\Models\User;
use App\Models\Wallet;
// ...
public function testUserIsShownCorrectly() {
$user = User::create(
[
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
]
);
Wallet::create(
[
'balance' => 0,
'user_id' => $user->id
]
);
$this->json('get', "api/user/$user->id")
->assertStatus(Response::HTTP_OK)
->assertExactJson(
[
'data' => [
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
'created_at' => (string)$user->created_at,
'wallet' => [
'id' => $user->wallet->id,
'balance' => $user->wallet->balance
]
]
]
);
}
Не забудьте включить операторы импорта в верхней части.
Обратите внимание, что на этот раз мы не только проверяем правильность возвращаемой структуры данных, но и проверяем точность возвращаемых данных.
Давайте добавим еще один тест для нашей destroy
функции:
public function testUserIsDestroyed() {
$userData =
[
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
];
$user = User::create(
$userData
);
$this->json('delete', "api/user/$user->id")
->assertNoContent();
$this->assertDatabaseMissing('users', $userData);
}
Эта функция проверяет, что API не возвращает никаких данных в ответе. Он также проверяет, что код ответа HTTP_NO_CONTENT
является кодом ответа (204
). У нас также есть утверждение, чтобы гарантировать, что пользователь был удален из базы данных.
Давайте напишем тест для нашей update
функции:
public function testUpdateUserReturnsCorrectData() {
$user = User::create(
[
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
]
);
Wallet::create(
[
'balance' => 0,
'user_id' => $user->id
]
);
$payload = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
];
$this->json('put', "api/user/$user->id", $payload)
->assertStatus(Response::HTTP_OK)
->assertExactJson(
[
'data' => [
'id' => $user->id,
'first_name' => $payload['first_name'],
'last_name' => $payload['last_name'],
'email' => $payload['email'],
'created_at' => (string)$user->created_at,
'wallet' => [
'id' => $user->wallet->id,
'balance' => $user->wallet->balance
]
]
]
);
}
В этом тесте мы создаем User
и Wallet
, а затем пытаемся обновить имя, фамилию и адрес электронной почты пользователя. Аналогично нашим предыдущим тестовым случаям, мы проверяем, что ответ HTTP_OK
(200
) возвращается и что содержание ответа является правильным.
Наконец, давайте напишем тестовый случай, чтобы убедиться, что все инвестиции для пользователя загружены правильно:
// ...
// Include import statements at the top
use App\Models\Investment;
use App\Models\Strategy;
// ...
public function testGetInvestmentsForUser() {
$user = User::create([
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
]
);
$strategy = Strategy::create(
Strategy::factory()->create()->getAttributes()
);
$isSuccessful = $this->faker->boolean;
$investmentAmount = $this->faker->randomNumber(6);
$investmentReturns = $isSuccessful ?
$investmentAmount * $strategy->yield :
$investmentAmount * $strategy->relief;
$investment = Investment::create(
[
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'successful' => $isSuccessful,
'amount' => $investmentAmount,
'returns' => $investmentReturns
]
);
$this->json('get', "api/user/$user->id/investments")
->assertStatus(Response::HTTP_OK)
->assertJson(
[
'data' => [
[
'id' => $investment->id,
'user_id' => $investment->user->id,
'strategy_id' => $investment->strategy->id,
'successful' => (bool)$investment->successful,
'amount' => $investment->amount,
'returns' => $investment->returns,
'created_at' => (string)$investment->created_at,
]
]
]
);
}
Не забудьте включить операторы импорта в верхней части.
В этой функции мы создаем новый User
, Strategy
, и Investment
. Затем мы проверяем, что он возвращается (в правильной структуре, с правильными данными), когда мы делаем запрос к investments
функции UserController
.
Запустите все свои тесты:
composer test
Вы должны увидеть что-то похожее на скриншот ниже, когда ваши тесты закончатся.
Давайте взглянем на наш отчет. Откройте tests/coverage/index.html
в своем браузере и перейдите к Controllers
.
UserController
теперь имеет полный охват. Однако означает ли это, что наш код не содержит ошибок?
Что, если мы попытаемся получить пользователя, которого не существует? Или что делать, если некоторые обязательные поля отсутствуют, когда мы пытаемся создать нового пользователя? Каков баланс кошелька, когда мы создаем нового пользователя? Давайте напишем несколько тестовых случаев с нашими ожиданиями для каждого сценария и посмотрим, что приложение возвращает.
Давайте добавим тестовые случаи для тех случаев , когда мы пытаемся вызвать методж show
, update
или delete
для отсутствующего пользователя. В каждом случае мы хотим вернуть HTTP_NOT_FOUND
код ошибки (404
) и убедиться, что наш ответ содержит error
запись.
public function testShowForMissingUser() {
$this->json('get', "api/user/0")
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testUpdateForMissingUser() {
$payload = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
];
$this->json('put', 'api/user/0', $payload)
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testDestroyForMissingUser() {
$this->json('delete', 'api/user/0')
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
Давайте также добавим тестовый случай, чтобы проверить, что когда запрос, отправленный в store
функцию, не содержит некоторых необходимых данных, HTTP_BAD_REQUEST
пользователю возвращается код ответа (400
).
public function testStoreWithMissingData() {
$payload = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName
//email address is missing
];
$this->json('post', 'api/user', $payload)
->assertStatus(Response::HTTP_BAD_REQUEST)
->assertJsonStructure(['error']);
}
Наконец, давайте добавим тестовый случай, чтобы гарантировать, что сохраненный пользователь всегда создается с пустым кошельком.
public function testStoredUserHasEmptyWallet() {
$payload = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->email
];
$apiResponse = $this
->json('post', 'api/user', $payload)
->getContent();
$userData = json_decode($apiResponse, true)['data'];
$walletDetails = $userData['wallet'];
$this->assertEquals(0, $walletDetails['balance']);
}
Запустите свои тесты еще раз и проверьте результаты:
Интересно, что наше приложение не могло справиться ни с одним из задуманных нами крайних случаев — даже несмотря на то, что изначально у нас было 100% покрытие кода.
Обновление приложения для прохождения тестов
Прежде чем обновлять код, давайте посмотрим, что происходит. Запустите свой сервер и либо с помощью браузера, либо с помощью postman перейдите по http://localhost:8000/api/user/0
ссылке . Вы увидите, что приложение возвращает HTML-ответ, когда пользователь не найден. Вам нужно переопределить ответ по умолчанию для тех случаев, когда модель с указанным идентификатором не может быть найдена.
Для этого откройте app/Exceptions/Handler.php
файл и обновите render
функцию, чтобы она соответствовала следующему:
use Illuminate\Http\Response;
// ...
public function render($request, Throwable $exception) {
if ($exception instanceof ModelNotFoundException) {
return response()->json(
[
'error' => 'Resource not found'
],
Response::HTTP_NOT_FOUND
);
}
return parent::render($request, $exception);
}
Это означает, что каждый раз, когда возникает это исключение, ответ JSON будет возвращен с записью, описывающей ошибку.
Проведите свои тесты еще раз. Вы увидите, что обработка ModelNotFoundException
разрешила 3 из наших предыдущих 5 неудач.
Далее, давайте попробуем исправить наш код так, чтобы он прошел testStoredUserHasEmptyWallet
тесты. Откройте app/Http/Controllers/UserController.php
файл и перейдите к store
функции. Вы увидите, что кошелек пользователя создан с балансом 100 вместо 0. Подобные ошибки часто встречаются в промышленности, и именно поэтому мы проводим тесты, чтобы убедиться, что мы поймаем их, прежде чем они пойдут в производство. Измените его на 0 и снова запустите тест.
Наконец, давайте исправим наш код, чтобы пройти testStoreWithMissingData
тест. Откройте app/Http/Controllers/UserController.php
файл и перейдите к store
функции. Мы проверим, являются ли first_name last_name email
null
и если да, то вернем ответ на ошибку. Обновите свою store
функцию, чтобы она соответствовала следующим требованиям:
use Illuminate\Http\Response;
// ...
public function store(Request $request) {
$firstName = $request->input('first_name');
$lastName = $request->input('last_name');
$email = $request->input('email');
if (is_null($firstName) || is_null($lastName) || is_null($email)) {
return $this->errorResponse(
'First Name, Last Name and Email are required',
Response::HTTP_BAD_REQUEST
);
}
$user = User::create(
[
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email
]
);
Wallet::create(
[
'balance' => 0,
'user_id' => $user->id
]
);
return $this->successResponse(
new UserResource($user),
true
);
}
Проведите свои тесты еще раз. На этот раз наш код проходит все тесты, и у нас также есть 100% покрытие кода для нашей UserController и User
модели.
Тестирование других конечных точек
Завершив тесты для UserController
, давайте продолжим делать то же самое для других наших контроллеров. В tests/Controllers
каталоге создайте новый файл с именем InvestmentControllerTests.php
. В вашем случае tests/Controllers/InvestmentControllerTests.php
добавьте следующее:
<?php
namespace Tests\Controllers;
use App\Models\Investment;
use App\Models\Strategy;
use App\Models\User;
use Illuminate\Http\Response;
use Tests\TestCase;
class InvestmentControllerTests extends TestCase {
public function testIndexReturnsDataInValidFormat() {
$this->json('get', 'api/investment')
->assertStatus(Response::HTTP_OK)
->assertJsonStructure(
[
'data' => [
'*' => [
'id',
'user_id',
'strategy_id',
'successful',
'amount',
'returns',
'created_at',
]
]
]
);
}
public function testInvestmentIsCreatedSuccessfully() {
$user = User::create(User::factory()->make()->getAttributes());
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$payload = [
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'amount' => $this->faker->randomNumber(4)
];
$this->json('post', 'api/investment', $payload)
->assertStatus(Response::HTTP_CREATED)
->assertJsonStructure(
[
'data' => [
'id',
'user_id',
'strategy_id',
'successful',
'amount',
'returns',
'created_at',
]
]
);
$this->assertDatabaseHas('investments', $payload);
}
public function testStoreWithMissingData() {
$payload = [
'amount' => $this->faker->randomNumber(4)
];
$this->json('post', 'api/investment', $payload)
->assertStatus(Response::HTTP_BAD_REQUEST)
->assertJsonStructure(['error']);
}
public function testStoreWithMissingUserAndStrategy() {
$payload = [
'user_id' => 0,
'strategy_id' => 0,
'amount' => $this->faker->randomNumber(4)
];
$this->json('post', 'api/investment', $payload)
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testInvestmentIsShownCorrectly() {
$user = User::create(User::factory()->make()->getAttributes());
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$isSuccessful = $this->faker->boolean;
$investmentAmount = $this->faker->randomNumber(6);
$investmentReturns = $isSuccessful ?
$investmentAmount * $strategy->yield :
$investmentAmount * $strategy->relief;
$investment = Investment::create(
[
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'successful' => $isSuccessful,
'amount' => $investmentAmount,
'returns' => $investmentReturns
]
);
$this->json('get', "api/investment/$investment->id")
->assertStatus(Response::HTTP_OK)
->assertExactJson(
[
'data' => [
'id' => $investment->id,
'user_id' => $investment->user->id,
'strategy_id' => $investment->strategy->id,
'successful' => $isSuccessful,
'amount' => round($investment->amount, 2, PHP_ROUND_HALF_UP),
'returns' => round($investment->returns, 2, PHP_ROUND_HALF_UP),
'created_at' => (string)$investment->created_at,
]
]
);
}
public function testShowMissingInvestment() {
$this->json('get', "api/investment/0")
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testDestroyInvestment() {
$user = User::create(User::factory()->make()->getAttributes());
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$isSuccessful = $this->faker->boolean;
$investmentAmount = $this->faker->randomNumber(6);
$investmentReturns = $isSuccessful ?
$investmentAmount * $strategy->yield :
$investmentAmount * $strategy->relief;
$investment = Investment::create(
[
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'successful' => $isSuccessful,
'amount' => $investmentAmount,
'returns' => $investmentReturns
]
);
$this->json('delete', "api/investment/$investment->id")
->assertStatus(Response::HTTP_UNAUTHORIZED);
}
public function testUpdateInvestment() {
$user = User::create(User::factory()->make()->getAttributes());
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$isSuccessful = $this->faker->boolean;
$investmentAmount = $this->faker->randomNumber(6);
$investmentReturns = $isSuccessful ?
$investmentAmount * $strategy->yield :
$investmentAmount * $strategy->relief;
$investment = Investment::create(
[
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'successful' => $isSuccessful,
'amount' => $investmentAmount,
'returns' => $investmentReturns
]
);
$payload = [
'id' => $investment->id,
'successful' => !$isSuccessful
];
$this->json('put', "api/investment/$investment->id", $payload)
->assertStatus(Response::HTTP_UNAUTHORIZED);
}
}
Наш InvestmentController
проходит большинство тестовых случаев, кроме testStoreWithMissingUserAndStrategy и testStoreWithMissingData
тестов. Чтобы исправить это, давайте обновим store
функцию в нашем app/Http/Controllers/InvestmentController.php
файле:
use App\Models\User;
// ...
public function store(Request $request) {
$userId = $request->input('user_id');
$strategyId = $request->input('strategy_id');
$amount = $request->input('amount');
if (is_null($userId) || is_null($strategyId) || is_null($amount)) {
return $this->errorResponse(
'User ID, Strategy ID and Amount are required',
Response::HTTP_BAD_REQUEST
);
}
$strategy = Strategy::findOrFail($strategyId);
$user = User::findOrFail($userId);
$investment = [
'user_id' => $user->id,
'strategy_id' => $strategy->id,
'amount' => $amount
];
$successful = (bool)random_int(0, 1);
$investment['successful'] = $successful;
$multiplier = $successful ?
$strategy->yield :
$strategy->relief;
$investment['returns'] = $amount * $multiplier;
$investment = Investment::create($investment);
return $this->successResponse(
new InvestmentResource($investment),
true
);
}
Как и в предыдущем случае UserController
, мы проверяем, чтобы убедиться, что все необходимые поля предоставлены, прежде чем пытаться создать новую Investment
модель. Мы также используем findOrFail
функцию, чтобы убедиться, что userIdstrategyId
предоставленны и действительно соответствуют сохраненному пользователю и стратегии в нашей базе данных. Если таковых не существует, будет создано исключение ModelNotFound
. Поскольку мы уже настроили наш обработчик для прослушивания этого исключения, наш API изящно завершит работу и вернет HTTP_NOT_FOUND код ошибки (404
) вместе с сообщением об ошибке.
Наконец, у нас есть тесты для наших StrategyController
. Создайте еще один файл в tests/Controllers
/StrategyControllerTests.php
.
<?php
namespace Tests\Controllers;
use App\Models\Strategy;
use Illuminate\Http\Response;
use Tests\TestCase;
class StrategyControllerTests extends TestCase {
public function testIndexReturnsDataInValidFormat() {
$this->json('get', 'api/strategy')
->assertStatus(Response::HTTP_OK)
->assertJsonStructure(
[
'data' => [
'*' => [
'id',
'type',
'tenure',
'yield',
'relief',
'investments' => [
'*' => [
'id',
'user_id',
'strategy_id',
'successful',
'amount',
'returns',
'created_at',
]
],
'created_at',
]
]
]
);
}
public function testStrategyIsCreatedSuccessfully() {
$payload = Strategy::factory()->make()->getAttributes();
$this->json('post', 'api/strategy', $payload)
->assertStatus(Response::HTTP_CREATED)->assertJsonStructure(
[
'data' => [
'id',
'type',
'tenure',
'yield',
'relief',
'investments',
'created_at',
]
]
);
$this->assertDatabaseHas('strategies', $payload);
}
public function testStrategyIsShownCorrectly() {
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$this->json('get', "api/strategy/$strategy->id")
->assertStatus(Response::HTTP_OK)
->assertExactJson(
[
'data' => [
'id' => $strategy->id,
'type' => $strategy->type,
'tenure' => $strategy->tenure,
'yield' => round($strategy->yield, 2, PHP_ROUND_HALF_UP),
'relief' => round($strategy->relief, 2, PHP_ROUND_HALF_UP),
'investments' => $strategy->investments,
'created_at' => (string)$strategy->created_at,
]
]
);
}
public function testUpdateMissingStrategy() {
$this->json('put', 'api/strategy/0', Strategy::factory()->make()->getAttributes())
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testDestroyMissingStrategy() {
$this->json('delete', 'api/strategy/0')
->assertStatus(Response::HTTP_NOT_FOUND)
->assertJsonStructure(['error']);
}
public function testStrategyIsUpdatedSuccessfully() {
$strategy = Strategy::create(Strategy::factory()->make()->getAttributes());
$payload = Strategy::factory()->make();
$this->json('put', "api/strategy/$strategy->id", $payload->getAttributes())
->assertStatus(Response::HTTP_OK)
->assertExactJson(
[
'data' => [
'id' => $strategy->id,
'type' => $payload['type'],
'tenure' => $payload['tenure'],
'yield' => round($payload['yield'], 2, PHP_ROUND_HALF_UP),
'relief' => round($payload['relief'], 2, PHP_ROUND_HALF_UP),
'investments' => $strategy->investments,
'created_at' => (string)$strategy->created_at,
]
]
);
}
public function testStrategyIsDestroyedSuccessfully() {
$strategyAttributes = Strategy::factory()->make()->getAttributes();
$strategy = Strategy::create($strategyAttributes);
$this->json('delete', "api/strategy/$strategy->id")
->assertStatus(Response::HTTP_NO_CONTENT)
->assertNoContent();
$this->assertDatabaseMissing('strategies', $strategyAttributes);
}
}
Благодаря этому у нас есть 100% – ный охват наших контроллеров, и мы также предусмотрели крайние случаи, чтобы гарантировать, что наше приложение способно обрабатывать их должным образом.
Вывод
В этом учебнике вы узнали, как писать тестовые случаи и измерять покрытие кода с помощью PHPUnit
.
Вы можете найти полную кодовую базу (с тестовыми случаями) здесь, на GitHub. Какие еще улучшения вы можете внести в приложение? Когда вы что-то найдете, вместо того чтобы просто обновлять контроллеры, попробуйте сделать следующее:
- Напишите тестовый случай для вашего улучшения (имитированный запрос и ожидаемый ответ).
- Запустите свои тесты (
composer test
) - Если какой-либо из ваших тестов провалится, напишите ровно столько кода, чтобы тест прошел успешно
- Посмотрите, сможете ли вы сделать свой код более понятным
- Запустите свои тесты еще раз
Тестирование – это необходимость, от которой мы не можем избавиться, если хотим создавать надежные приложения. Полное принятие TDD требует изменения менталитета. Вместо того чтобы говорить себе: “я перейду этот мост, когда доберусь туда”, спросите себя: “какие мосты я должен пересечь и как я должен их пересечь?”.
Подскажите из за чего может быть причина того что при запуске composer test выдает следующее: Generating code coverage report in HTML format … Class “App\Models\Model” not found. Пытаюсь приделать к существующему проекту… на примере с данного сайта все работает как надо
в тексте ошибки у вас написан ответ. Модель не найдена. проверьте ваш код, верно ли все написано. Запусакается ли проверяемый код руками?