TDD в PHP, ч. 1: як почати писати код через Unit-тести

6 хв. читання

Ця перша стаття в серії матеріалів про Test-Drive Development (TDD).

Посилання на всі статті:

  1. TDD в PHP частина №1 – як почати писати код через Unit-тести
  2. TDD в PHP частина №2 – інструменти для написання Unit-тестів
  3. TDD в PHP частина №3 – що і як варто покривати тестами

Що таке Unit-тести?

Unit-тести – лише один з можливих способів тестування коду. Він показує чи код працює як очікується. Це дозволяє виявити проблему ще на стадії розробки й зекономити час на її пошук і виправлення.

На жаль, юніти не можуть виявити всіх проблем, які виникають. \ Наприклад, якщо проводити аналогію з дверима, то це як перевіряти чи повертається клямка – вона може справно працювати й відводити язичок замка для відкриття дверей і юніт покаже, що двері справні. Але він не зможе показати чи відкриваються двері, бо на них можуть впливати зовнішні чинники, наприклад робочий стіл дизайнера, який захотів поставити його саме за дверима ;)

Хибне уявлення про TDD

«Розробка через написання юнітів» звучить красиво, але зазвичай асоціюється у програмістів з наступним (не правильним) процесом:

  1. Написати повністю код
  2. Написати повністю тести
  3. Запустити тести

В різних версіях, перший і другий пункти міняються місцями, до процесу залучаються аналітики й/та QA, але суть залишається одна – цей процес практично утопічний. Мало яка компанія дозволить собі такі витрати на код, що обслуговує, який ще й не все може перевірити. Мені було б цікаво дізнатися, які компанії працюють саме за такою схемою, скільки на це витрачається часу і який результат (чи приносить це якусь користь в цифрах).

Нормальний процес TDD

Основний принцип полягає в отриманні якомога раніше помилки і її виправленні (майже як в Scrum щодо зворотнього зв'язку). Цей процес складається з 3-х кроків і повторюється циклічно:

  1. Червоний (red) – отримання помилки,
  2. Зелений (green) – помилку виправлено, тест пройшов успішно,
  3. Вдосконалення (refactoring) – змінюємо код і переходимо знову на перший крок.

Процес відбувається не до і не після написання логіки, а під час. Тобто кожна функція, кожен метод розбивається на логічні складові (отримання, опрацювання даних, збереження тощо) і розробляються по черзі. Написали перевірку вхідних даних – протестували, написали наступну міні-частину коду – протестували. Такий підхід дозволяє писати і тестувати код одночасно і поступово.

По правді, спочатку трохи складно перебудуватися – потрібно інакше мислити. Сам принцип проектування методів і класів інакший.

Та коли призвичаїтися, це не має займати багато додаткового часу, оскільки код і так потрібно тестувати в ході розробки вручну чи через юніти (код варто запускати ще до того, як написана вся логіка, щоб перевірити чи правильно сформовані масиви, валідність запитів в базу, банально чи не забута ;). Так чому б цей процес не зробити через код, щоб його можна було легко відтворити в майбутньому!?

Пишемо простий тест

Для тестів я використовую фреймворк PHPUnit + localhost + PHPStorm. Всі тести для статті я виконував на PHP 7.0.28, для інших версій неймспейси й назви класів фреймворку можуть відрізнятися, але суть від цього не змінюється (документація: https://phpunit.readthedocs.io/en/7.1/)

Почнемо з того, що визначимо функціональність та інтерфейс майбутнього класу. \ Припустимо, нам потрібно реалізувати рейтинги для користувачів. Логіка має містити можливості додавання/знімання рейтингу. Як мінімум в нас має бути 2 методи add_rating() i subtract_rating().

Для початку реалізовуємо інтерфейс для першого методу:

require_once 'user.php';

use PHPUnit\\Framework\\TestCase;

class usetTest extends TestCase {
	public function test_add_rating()
	{
		$user = new user();
		$user->add_rating(10);
	}
}

//Вміст файлу `user.php`

class user
{
	public function __construct() {}
	
	public function add_rating($add_rating) {}
}

Виконавши цей тест ми отримаємо наступну помилку: This test did not perform any assertions. Це означає, що тест не розуміє, що йому перевіряти (порівнювати). Додамо у наш тест один з методів ствердження assertEquals($param1, $param2) – ствердження рівності $param1 == $param2:

require_once 'user.php';

use PHPUnit\\Framework\\TestCase;

class usetTest extends TestCase {
	public function test_add_rating()
	{
		$user = new user();
		
		$this->assertEquals($user->add_rating(10), 10);
	}
}

//Вміст файлу `user.php`

class user
{
	public function __construct() {}

	public function add_rating($add_rating) {}
}

Тест виведе помилку:

Failed asserting that 10 matches expected 0.
Expected :0
Actual   :10

Це означає, що рейтинг користувача не змінився. Виправимо це:

require_once 'user.php';

use PHPUnit\\Framework\\TestCase;

class usetTest extends TestCase {
	public function test_add_rating()
	{
		$user = new user();

		$this->assertEquals($user->add_rating(10), 10);
	}
}

//Вміст файлу `user.php`

class user
{
	public function __construct() {}

	public function add_rating($add_rating) {
		return (int) $add_rating;
	}
}

Цей тест виконався успішно і вивів повідомлення OK (1 test, 1 assertion). Але його проблема в тому, що помилка була виправлена хаком. Тому тепер нам потрібно покращити код:

require_once 'user.php';

use PHPUnit\\Framework\\TestCase;

class usetTest extends TestCase {
	public function test_add_rating()
	{
		$user = new user();
		$user->add_rating(10);

		$this->assertEquals($user->rating, 10);
	}
}

//Вміст файлу `user.php`

class user
{
	private $rating = 0;

	public function __construct() {}

	public function __get($property_name)
	{
		switch ($property_name) {
			case 'rating':
				return $this->$property_name;

			default:
				throw new InvalidArgumentException("Property `{$property_name}` does not exist");
		}
	}

	public function add_rating($add_rating) {
		$this->rating += (int) $add_rating;
	}
}

Тепер add_rating() буде працювати як потрібно.

З методом subtract_rating все так само: спочатку пишемо, що ми від нього очікуємо, потім реалізовуємо сам метод і покращуємо первинну логіку.

Структура UNIT-тесту

  • підготовка,
  • дія,
  • ствердження.

В коді це виглядає так:

require_once 'user.php';

use PHPUnit\\Framework\\TestCase;

class usetTest extends TestCase {
	/**
	 * Перевірка додавання рейтингу
	 */
	public function test_add_rating()
	{
		//Підготовка
		$user = new user();

		//Дія
		$user->add_rating(10);

		//Ствердження
		$this->assertEquals($user->rating, 10);
	}

	/**
	 * Перевірка віднімання рейтингу
	 */
	public function test_subtract_rating()
	{
		//Підготовка
		$user = new user();
		$user->add_rating(10);

		//Дія
		$user->subtract_rating(3);

		//Ствердження
		$this->assertEquals($user->rating, 7);
	}

	/**
	 * Перевірка логіки, коли віднімається більше рейтингу, ніж є у користувача
	 */
	public function test_subtract_rating_negative_value()
	{
		//Підготовка
		$user = new user();
		$user->add_rating(10);

		//Дія
		$user->subtract_rating(13);

		//Ствердження
		$this->assertEquals($user->rating, 0);
	}
}

//Вміст файлу `user.php`

class user
{
	private $rating = 0;

	public function __construct() {}

	public function __get($property_name)
	{
		switch ($property_name) {
			case 'rating':
				return $this->$property_name;

			default:
				throw new InvalidArgumentException("Property `{$property_name}` does not exist");
		}
	}

	public function add_rating($add_rating) {
		$this->rating += (int) $add_rating;
	}

	public function subtract_rating($subtract_rating) {
		$this->rating -= (int) $subtract_rating;

		if ($this->rating < 0) {
			$this->rating = 0;
		}
	}
}

Думаю, основний принцип зрозумілий.

Як покрити наявний код тестами

Я бачу 2 можливих варіанти:

  1. Покрити все і зразу \ Це може затягнутися на кільки тижнів/місяців/років. Тому я не впевнений, що це той шлях, який приведе до результату. Бізнес просто не дасть стільки працювати над технічним боргом і захоче чогось, що напряму дає фінансовий результат.

  2. Покривати код поступово \ Код, рано чи пізно, потребує рефакторингу, і це чудова нагода перебудувати його і покрити тестами. Без лишнього героїзму й особливих просідань у швидкості розробки нового функціоналу.


Анонс \ В наступній статті буде більш детально розказано, як налаштувати робоче місце, про сам фреймворк PHPUnit і нюанси роботи з ним.

Чистого коду і попутних завдань!

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 639
Приєднався: 1 рік тому
Коментарі (1)
Щоб залишити коментар необхідно авторизуватися.

Вхід