Что такое TDD?
TDD (Test-Driven Development) - это подход к разработке, когда вы сначала пишете тест, а потом код, который делает этот тест успешным.
Процесс состоит из 3 шагов, которые называют "Красный-Зеленый-Рефакторинг":
- Красный - Пишем тест, который падает (потому что кода еще нет)
- Зеленый - Пишем минимальный код, чтобы тест прошел
- Рефакторинг - Улучшаем код, сохраняя тесты зелеными
Для чего это нужно?
Это как ускоряет разработку, так и повышает надежность кода + избавляет от необходимости писать автотесты в дальнейшем.
Можно подумать, что разработка, наоборот, затягивается так как помимо написание функционала еще нужно тратить время на написание теста, но на самом деле это не так. В случае если вы пишете функционал, например, какой-то api роут. То написание идет примерно по следующей схеме:
1. Написание части функционала
2. Составление запроса в Postman
3. Запрос падает и вы сверяете трейс и ищите нужный функционал
4. Вносите исправление
5. Снова проверяете и в этот раз все хорошо
6. Пушите изменения в dev и снова составляете запрос в Postman, но уже на dev сервере
7. В случае если на dev возникла ошибка, то цикл повторяется, а в случае если ошибки нет, то он скорее всего повторится когда всплывет какой-либо баг
То есть на дистанции жизненного цикла приложения это будет отнимать только лишь больше времени. А если еще в дальнейшем будет задача покрыть текущий код тестами, то тут и времени много затратится и желания будет немного выполнять столь рутинную работу.
В случае с TDD подходом вы двигаетесь по следующему сценарию:
1. Пишете строчку теста
2. Запускаете тест и он не отрабатывает
3. Пишите строчку кода
4. Запускаете тест и в этот раз он отрабатывает
И так по кругу пока не будет написан весь функционал.
Плюсы:
- Все в рамках IDE. В левом стороне редактора можно вывести тест. В правой стороне сам функционал. Снизу консоль где запускаются тесты. Не нужно никуда переключаться, составлять запросы в Postman или аналогах и так далее
- Сразу понимаете где ошибка. Если тест вы написали строку, которая положила все тесты, то вы вносите изменения сразу в этой строке
- Вы имеете сразу покрытый тестами код и при необходимости внесения дальнейший доработок или его рефакторинга, вы уже будете уверены, что ничего не сломали
- Тесты можно повесить на CI/CD тем самым делегируя проверку функциональности на сервер, а не на ваши мануальные проверки
- Любой разработчик не смотря в код уже понимает все кейсы его использования. Например, когда упадет 404, когда 403, когда 429 и так далее, так как все это покрыто тестами.
Практический пример: Создание пользователя через REST API (Laravel)
Шаг 1: Пишем падающий тест
Сначала создадим тест для эндпоинта создания пользователя:
class UserCreationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_creates_a_new_user()
{
// Подготовка данных
$userData = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123'
];
// Действие - отправляем POST запрос
$response = $this->postJson('/api/users', $userData);
// Проверки
$response->assertStatus(201) // Должен вернуть статус 201 Created
->assertJsonStructure([
'data' => [
'id',
'name',
'email',
'created_at'
]
]);
// Проверяем, что пользователь действительно создался в БД
$this->assertDatabaseHas('users', [
'email' => 'john@example.com'
]);
}
}
Запускаем тест:
php artisan test tests/Feature/UserCreationTest.php
Тест падает - потому что эндпоинта /api/users еще не существует!
Шаг 2: Пишем минимальный код для прохождения теста
Создаем маршрут и контроллер:
1. routes/api.php:
Route::post('/users', [UserController::class, 'store']);
2. app/Http/Controllers/UserController.php:
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
]);
$user = User::create($validated);
return response()->json([
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'created_at' => $user->created_at
]
], 201);
}
}
Запускаем тест снова:
php artisan test tests/Feature/UserCreationTest.php
Тест проходит!
Шаг 3: Рефакторинг (улучшаем код)
Теперь можно улучшить код, не боясь сломать функциональность:
// Улучшенный контроллер
class UserController extends Controller
{
public function store(CreateUserRequest $request): JsonResponse
{
$user = User::create($request->validated());
return response()->json([
'data' => new UserResource($user)
], 201);
}
}
// Создаем Form Request для валидации
// app/Http/Requests/CreateUserRequest.php
class CreateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
];
}
}
// Создаем Resource для преобразования данных
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at
];
}
}
Запускаем тест еще раз - все еще зеленый!
Дополняем тестами крайние случаи
Теперь добавляем тесты для обработки ошибок:
/** @test */
public function it_returns_error_when_email_already_exists()
{
// Сначала создаем пользователя
User::factory()->create(['email' => 'existing@example.com']);
$userData = [
'name' => 'John Doe',
'email' => 'existing@example.com', // Дублирующий email
'password' => 'password123'
];
$response = $this->postJson('/api/users', $userData);
$response->assertStatus(422) // Unprocessable Entity
->assertJsonValidationErrors(['email']);
}
/** @test */
public function it_returns_error_when_required_fields_are_missing()
{
$userData = [
'name' => 'John Doe'
// Пропускаем email и password
];
$response = $this->postJson('/api/users', $userData);
$response->assertStatus(422)
->assertJsonValidationErrors(['email', 'password']);
}