Привет, меня зовут Артем Рыбин, и я team lead backend developer в KOTELOV. Сегодня я покажу, как решить практическую задачку с помощью нейросети и сэкономить больше часа. Будем покрывать unit тестами DTO класс в приложении на Laravel
Это будет серия статей, поэтому подписывайтесь на телегу kotelov_love, чтобы ничего не пропустить.
Рабочую схему “как попасть в chat openai” я расписывать не буду, их итак полно на просторах “всемирной”, поэтому просто советую найти ту, которая больше понравится.
Как вообще лиду пришла такая идея в голову?
Как только весь хайп с нейросетками утих, а мемасики стали появляться гораздо реже, я решил подумать, чем действительно может быть полезен ChatGPT.
Первое, на что обратил внимание искусственного интеллекта, — это рутина, которую мне лень делать руками. А что всем нам лень делать? Правильно, тесты писать.
Все падаваны заняты своими заботами, поэтому не будут писать тесты за меня, а делать это всё-таки надо…
Поэтому что? Правильно, сажусь и пишу сам иду в чат и прошу ИИ сделать работу за меня.
Взял для задачки простенький пример, который сам бы писал руками час.
Дано:
Приложение на laravel
Класс DTO, который надо покрыть unit тестами
Лень
Идеальная комбинация для того, чтобы работать с нейросетями.
Для написания unit теста не нужно скармливать всё приложение, поэтому я взял простенький DTO класс и попросил чат сформировать тесты на него.
class UserCrmDto
{
public function __construct(
private readonly array $data,
) {
}
public function getPhone(): string
{
return Arr::get($this->data, 'clnt_MobPhone');
}
public function getExternalGuid(): string
{
return Arr::get($this->data, 'clnt_ID');
}
public function getPassword(): string
{
return Arr::get($this->data, 'clnt_Pwd');
}
public function getEmail(): string
{
return Arr::get($this->data, 'clnt_EMail');
}
public function getSurname(): string
{
return Arr::get($this->data, 'clnt_Surname');
}
public function getName(): string
{
return Arr::get($this->data, 'clnt_Name');
}
public function getPatronymic(): string
{
return Arr::get($this->data, 'clnt_Patronymic');
}
}
Класс, над которым проводим эксперимент
Намеренно делаю это обычным промптом. Для наглядности сначала буду давать ему запросы как мидлу, а под конец начну давать задачи, как будто это джун. Обратите внимание на разницу результатов.
Первая же попытка приносит неплохой результат, но не совсем то, что нужно. Однако, путь верный. Вот что получилось (часть кода специально опущена для компактности):
>> Напиши unit тесты на phpunit для следующего класса
use PHPUnit\Framework\TestCase;
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function testGetPhone(): void
{
$expected = '123456789';
$actual = $this->userCrmDto->getPhone();
$this->assertEquals($expected, $actual);
}
public function testGetExternalGuid(): void
{
$expected = 'abc123';
$actual = $this->userCrmDto->getExternalGuid();
$this->assertEquals($expected, $actual);
}
// И так далее…
}
Первая итерация
В некоторых случаях ИИ отдает неполный вариант с комментарием: «// … Другие методы тестирование».
Как бы говоря: ”Иди допиши остальное”. Если бы я хотел делать это сам, я бы к тебе даже не обращался…
Будем умнее машины и попросим увеличить покрытие до 100%. На выходе имеем класс с покрытием под 100% — шикарно. Вот как получилось:
>> Сделай покрытие тестами 100%
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function testGetPhone(): void
{
$expected = '123456789';
$actual = $this->userCrmDto->getPhone();
$this->assertEquals($expected, $actual);
}
// И так до 100% покрытия…
}
Вторая итерация со 100% покрытием
Всё бы ничего, но в проекте у нас laravel, и чтобы не переписывать руками, я попросил сразу переделать код под него.
>> Переделай тест на работу с фреймворком laravel
use Illuminate\Foundation\Testing\TestCase;
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
parent::setUp();
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
public function testGetPhone()
{
$expected = '123456789';
$actual = $this->userCrmDto->getPhone();
$this->assertEquals($expected, $actual);
}
// Остальные методы..
}
Третья итерация, рефакторинг под laravel
Тут уже интереснее, замечаем, что он добавляет строки, которые совсем не нужны:
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
Я получил вполне приемлемый результат для работы, после того как я попросил избавиться от них, но и это еще не все. Надо добавить проверку на невалидные значения.
>> Разве нам нужен createApplication ? Мы же пишем unit
>> Можешь добавить проверку на невалидные значения?
use PHPUnit\Framework\TestCase;
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function testGetPhone(): void
{
$expected = '123456789';
$actual = $this->userCrmDto->getPhone();
$this->assertEquals($expected, $actual);
}
// Основные тесты…
public function testInvalidPhoneValue(): void
{
$invalidData = ['clnt_MobPhone' => 123456789];
$userCrmDto = new UserCrmDto($invalidData);
$this->expectException(\TypeError::class);
$userCrmDto->getPhone();
}
// Тесты на некорректные значения…
}
Четвертая итерация, добавление проверки невалидных значений
Всё бы ничего, но тут он забыл про laravel… Ну ничего, мне совсем не сложно напомнить ему об этом.
>> Мне кажется, ты забыл, что мы работаем с laravel
use Illuminate\Foundation\Testing\TestCase;
use Illuminate\Support\Arr;
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
parent::setUp();
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
public function testGetPhone()
{
$expected = '123456789';
$actual = $this->userCrmDto->getPhone();
$this->assertEquals($expected, $actual);
}
// Остальные методы…
}
Пятая итерация, вспоминаем про laravel
И снова bootstrap. Не знаю, почему он забыл, что мы пишем unit, и куда потерял синтаксис php 8.1, но не пытаясь его понять, просто говорю переделать. Это, кстати, проще, чем просить коллегу в pull request.
>> Отлично, теперь давай перепишем в синтаксис php 8.1 и всё-таки уберем работу с bootstrap, так как он не нужен при написании unit
class UserCrmDtoTest extends TestCase
{
private array $data;
private UserCrmDto $userCrmDto;
protected function setUp(): void
{
$this->data = [
'clnt_MobPhone' => '123456789',
'clnt_ID' => 'abc123',
'clnt_Pwd' => 'password123',
'clnt_EMail' => 'test@example.com',
'clnt_Surname' => 'Doe',
'clnt_Name' => 'John',
'clnt_Patronymic' => 'Smith',
];
$this->userCrmDto = new UserCrmDto($this->data);
}
public function testGetPhone(): void
{
$this->assertSame('123456789', $this->userCrmDto->getPhone());
}
public function testGetExternalGuid(): void
{
$this->assertSame('abc123', $this->userCrmDto->getExternalGuid());
}
public function testGetPassword(): void
{
$this->assertSame('password123', $this->userCrmDto->getPassword());
}
public function testGetEmail(): void
{
$this->assertSame('test@example.com', $this->userCrmDto->getEmail());
}
public function testGetSurname(): void
{
$this->assertSame('Doe', $this->userCrmDto->getSurname());
}
public function testGetName(): void
{
$this->assertSame('John', $this->userCrmDto->getName());
}
public function testGetPatronymic(): void
{
$this->assertSame('Smith', $this->userCrmDto->getPatronymic());
}
public function testInvalidPhoneValue(): void
{
$invalidData = ['clnt_MobPhone' => 123456789];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingPhoneValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidExternalGuidValue(): void
{
$invalidData = ['clnt_ID' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingExternalGuidValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidPasswordValue(): void
{
$invalidData = ['clnt_Pwd' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingPasswordValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidEmailValue(): void
{
$invalidData = ['clnt_EMail' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingEmailValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidSurnameValue(): void
{
$invalidData = ['clnt_Surname' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingSurnameValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidNameValue(): void
{
$invalidData = ['clnt_Name' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingNameValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
public function testInvalidPatronymicValue(): void
{
$invalidData = ['clnt_Patronymic' => 123];
$this->expectException(\TypeError::class);
new UserCrmDto($invalidData);
}
public function testMissingPatronymicValue(): void
{
$missingData = [];
$this->expectException(\InvalidArgumentException::class);
new UserCrmDto($missingData);
}
}
Финальный класс
Вот она, долгожданная финальная структура теста, который можно затащить к себе в проект, и отписать в задаче, что потратили на это 2 часа времени перейти к написанию новых фич.
На самом деле запрос мог выглядеть куда более компактно и принес бы точно такой же результат. Но я хотел понять, насколько быстро он может решать проблемы без контекста.
Вот итоговый промпт:
>> Напиши мне тесты на данный класс со 100% покрытием, проверками на невалидные значения, использованием синтаксиса php 8.1 и оптимизируй запрос под фреймворк laravel
Я бы мог продублировать финальный вариант ответа от ИИ, но поверьте на слово, он ничем не отличается от того, чему мы его учили.
Что мы имеем в сухом остатке: чат достаточно неплохо справляется с решением примитивных задач, как в примере выше. Использовать его или нет - это только ваше дело, но лично я думаю, что буду применять его в своей работе для экономии времени.
Если хотите читать больше таких статей — подписывайтесь на телеграм-канал kotelov_love. А я в свою очередь, постараюсь подготовить еще немного материала в котором покажу, как нейросети могут приносить реальную пользу в разработке.