Як писати код, що легко тестується і підтримується на PHP, PHP
Різноманітні фреймворки надають інструменти швидкої розробки додатків, але найчастіше вони сприяють накопиченню технічного боргу також швидко, як дозволяють створювати функціональність.
Технічний обов'язок виникає, коли супровід не є головною метою розробника. Майбутні зміни та налагодження коду стають скрутними у зв'язку з недостатнім модульним тестуванням та неопрацьованою структурою.
У цій статті ви дізнаєтеся, як структурувати ваш код так, щоб досягти простоти тестування та супроводу – і заощадити ваш час.
- Принцип "Не повторюйся";
- Використання залежності;
- Інтерфейси;
- Контейнери;
- Модульні тести за допомогою фреймворку PHPUnit.
Почнемо з дещо вигаданого, але типового коду. Це може бути клас у будь-якому заданому фреймворку:
Цей код буде працювати, але потребує деяких покращень:
1. Цей код не тестується.
Ми покладаємося на глобальну змінну $_SESSION. Фреймворки модульного тестування, такі як PHPUnit, працюють із командним рядком, де $_SESSION та інші глобальні змінні не доступні. Ми покладаємося на з'єднання з базою даних. В ідеалі, в модульному тестуванні необхідно уникати реальних з'єднань з базою даних. Тестування пов'язане з кодом, а чи не з даними.
2. Цей код не такий підтримуваний, яким би міг бути.
Наприклад, якщо ми змінимо джерело даних, нам потрібно буде змінювати код бази даних при кожному використанні App::db у нашому додатку. Крім того, незрозуміло, як бути у випадку, коли ми хочемо використовувати не просто інформацію про поточного користувача?
Здійснено модульний тест
Тут наведено спробу створення модульного тесту для функціоналу, описаного вище:
Давайте розглянемо цей тест. По-перше, він провалиться. Змінна $_SESSION , що використовується в класі User , не існує в модульному тесті, оскільки він запускає код командного рядка.
По-друге, немає встановлення з'єднання з базою даних. Це означає, що для того, щоб все запрацювало, нам потрібно буде запустити нашу програму, щоб отримати об'єкти App і db. Також нам потрібне активне з'єднання з базою даних для тестування.
Для того, щоб цей модульний тест працював, нам потрібно зробити таке:
- Встановити налаштування конфігурації нашої програми для запуску інтерфейсу командного рядка (PHPUnit);
- Довіряти підключенню до бази даних. Це означає розглядати джерело даних окремо від нашого модульного тесту. Що, якщо наша тестова база даних не містить очікуваних нами даних? Що робити, якщо ми маємо повільне з'єднання з базою даних?
- Покладаючись на те, що програма завантажується, ми збільшуємо накладні витрати тестування, різко уповільнюючи модульні тести. В ідеалі більша частина нашого коду має бути протестована незалежно від використаного фреймворку.
Що ж, перейдемо до того, як це можна поліпшити.
Дотримуємося принципу "Не повторюйся"
Функція, що повертає поточного користувача, не є необхідною у контексті нашого простого прикладу. Це вигаданий приклад, але дотримуючись принципу «Не повторюйся», я вибираю як першу оптимізацію узагальнення даного методу:
Цим методом ми можемо користуватися у всьому нашому додатку. Ми можемо передати поточного користувача у виклик функції, замість того, щоб впроваджувати даний функціонал у нашу модель. Код стаєбільш модульним та підтримуваним, коли перестає залежати від інших функціональних одиниць (наприклад, глобальної змінної сесії).
Однак він все ще не такий, що легко тестується і підтримується, яким би міг бути. Ми досі покладаємось на з'єднання з базою даних.
Використання залежності
Давайте поправимо ситуацію шляхом додавання деяких залежностей. Тут показано, як може виглядати наша модель, якщо ми помістимо з'єднання з базою даних до класу:
Тепер залежність для нашої моделі User забезпечена. Наш клас більше не передбачає наявність певного підключення до бази даних і не покладається на будь-які глобальні об'єкти.
На даний момент наш клас загалом тестований. Ми можемо передати джерело даних згідно з нашим вибором (здебільшого) та ідентифікатор користувача та протестувати результати виклику. Також ми можемо перемикати з'єднання до окремих баз даних (припускаючи, що обидві бази реалізують однакові методи вилучення даних). Круто!
Давайте подивимося, як виглядає модульний тест зараз:
Я додав щось нове в цей модульний тест: фіктивну реалізацію. Фіктивна реалізація дозволяє нам імітувати (фальшувати) PHP об'єкти. В даному випадку ми здійснюємо фіктивну реалізацію підключення до бази даних. З нашою заглушкою ми можемо пропустити тестування підключення до бази даних і просто перевірити нашу модель.
Хочете дізнатися більше про фіктивну реалізацію?
У разі ми імітуємо SQL підключення. Ми вказуємо, що фіктивний об'єкт має методи select, where, limit та get. Я повертаю сам фіктивний об'єкт, щоб відобразити, як об'єкт підключення SQL повертає себе ( $this ). Таким чином, виклики цього стають ланцюговими. Зазначимо,що для методу get я повертаю результат виклику бази даних – об'єкт класу stdClass із даними користувача.
Таким чином, вирішуються кілька проблем:
- Ми тестуємо лише нашу модель класу. Ми не перевіряємо підключення до бази даних;
- Ми можемо контролювати вхідні та вихідні дані фіктивного підключення до бази даних, а отже, провести надійне тестування, не звертаючи уваги на результат запиту в базі даних. Я знаю, що я отримаю ідентифікатор користувача "1" як результат фіктивного звернення до бази даних;
- Нам не потрібно запускати нашу програму, або мати будь-яку конфігурацію або існуючу базу даних для того, щоб проводити тестування.
Але ми все ще можемо зробити наш код набагато краще. З цього місця починається найцікавіше.
Інтерфейси
Для подальшого покращення ми можемо визначити та реалізувати інтерфейси. Розглянемо наступний код:
Тут трапляється кілька речей.
- По-перше, ми визначаємо інтерфейс нашого джерела даних. Таким чином, визначається метод getUser();
- Потім ми реалізуємо цей інтерфейс. В даному випадку ми здійснюємо реалізацію MySQL. Ми приймаємо об'єкт підключення до бази даних, та, використовуючи його, витягуємо інформацію про користувача з бази даних;
- І, нарешті, ми налаштовуємо використання реалізації класу UserRepositoryInterface у нашій моделі User . Цим гарантується, що джерело даних завжди матиме доступний метод getUser(), незалежно від того, яке саме джерело даних використовується для реалізації UserRepositoryInterface .
Зауважте, що наш об'єкт класу User містить типизацію для об'єктів UserRepositoryInterface у своєму конструкторі. Це означає, що клас,реалізуючий UserRepositoryInterface , повинен бути переданий в об'єкт класу User. Це гарантує те, що метод getUser , який ми покладаємося, буде завжди доступний.
Що ми маємо у результаті?
- Наш код тепер повністю тестований. Для класу User ми легко імітуємо джерело даних (тестування реалізації джерела даних завдання окремого модульного тесту);
- Наш код став набагато більш підтримуваним. Ми можемо підключати різні джерела даних, не вносячи зміни до коду у всьому нашому додатку;
- Ми можемо створити БУДЬ-ЯКЕ джерело даних: ArrayUser, MongoDbUser, CouchDbUser, MemoryUser і т.д.;
- Ми можемо легко передати будь-яке джерело даних в об'єкт нашого класу User, якщо це потрібно. Якщо ви вирішите уникнути бази даних SQL , ви можете просто створити іншу реалізацію (наприклад, MongoDbUser ) і передати її у вашу модель User .
А ще ми спростили наш модульний тест!
Ми повністю позбулися фіктивного підключення до бази даних. Натомість ми просто імітуємо джерело даних і говоримо йому, що робити, коли викликається метод getUser .
Але ми все ще можемо зробити код кращим!
Контейнери
Розглянемо використання нашого поточного коду:
І це навряд чи відповідає принципу "Не повторюйся". Контейнери можуть виправити це.
Контейнер просто містить у собі об'єкт чи функцію. Це схоже на реєстр у вашому додатку. Ми можемо використовувати контейнер для автоматичного створення нового об'єкта User із усіма необхідними залежностями. Нижче я використовую поширений клас контейнерів Pimple.
Я перемістив створення моделі User в одне місце у конфігурації програми. В результаті:
- Наш код відповідає принципу "Не повторюйся". Об'єкткласу User та обране місце зберігання даних визначено в одному місці нашої програми;
- Ми можемо перемикати нашу модель User від використання MySQL до іншого джерела даних в одному місці. Це значно полегшує підтримку коду.
Заключне слово
У нашому уроці ми виконали таке:
- Підкорили наш код принципу «Не повторюйся» і уможливили його повторне використання;
- Створили код, що підтримується – ми можемо, якщо потрібно, перемикати джерела даних для наших об'єктів для всієї програми в одному місці;
- Зробили наш код легко тестованим – ми можемо просто імітувати об'єкти, не покладаючись на завантаження нашої програми або створення тестової бази даних;
- Отримали знання про впровадження залежностей та інтерфейси для того, щоб створювати код, що легко тестується і підтримується;
- Побачили на прикладі, як контейнери можуть допомогти зробити нашу програму легшою в обслуговуванні.
Я впевнений, ви помітили, що ми додали набагато більше коду для досягнення простоти обслуговування та тестування. Сильний аргумент проти такої реалізації – збільшення складності. Справді, цього потрібно глибше знання коду, як основного розробника, так інших учасників проекту.
Проте, витрати на пояснення та розуміння коду з лишком окупаються зниженням технічного боргу. Код став значно легшим в обслуговуванні, представилася можливість проводити зміни лише в одному місці, а не в кількох.
Можливість швидко проводити модульне тестування знизить кількість помилок у коді із значним відривом – особливо у довгострокових або розроблюваних у співтоваристві (з відкритим вихідним кодом) проектах.
Виконуючидодаткову роботу зараз, ми заощадимо час і звільнимось від головного болю у майбутньому.
Ви можете легко включити фіктивні об'єкти та PHPUnit у вашу програму за допомогою Composer . Додайте ось це у вашу секцію “require-dev” у файлі composer.json:
Потім ви можете встановити ваші залежності, що базуються на Composer, з наступними вимогами:
При використанні PHP фреймворку Laravel 4 застосування контейнерів та інших ідей, описаних тут, має виключно важливий характер.
Дякую за прочитання!
Дана публікація є перекладом статті «How to Write Testable and Maintainable Code in PHP», підготовленої дружньою командою проекту Інтернет-технології.