Как да напишете лесен за тестване и поддръжка код в PHP, PHP

Различни рамки предоставят инструменти за бързо разработване на приложения, но те често натрупват технически дълг толкова бързо, колкото ви позволяват да създавате функционалност.

Технически дълг възниква, когато поддръжката не е основната цел на предприемача. Бъдещите промени и отстраняването на грешки в кода стават трудни поради недостатъчно модулно тестване и неразработена структура.

В тази статия ще научите как да структурирате кода си за лесна възможност за тестване и поддръжка – и ще ви спести време.

  • Принципът на "Не повтаряй";
  • Инжектиране на зависимост;
  • интерфейси;
  • Контейнери;
  • Единични тестове с PHPUnit framework.

Нека започнем с малко измислен, но типичен код. Това може да бъде клас във всяка дадена рамка:

Този код ще работи, но се нуждае от известна корекция:

1. Този код не е тестван.

Разчитаме на глобалната променлива $_SESSION. Рамките за модулно тестване като PHPUnit работят на командния ред, където $_SESSION и други глобални променливи не са налични. Разчитаме на връзки с бази данни. В идеалния случай модулното тестване трябва да избягва връзки с реални бази данни. Тестването е за код, а не за данни.

2. Този код не се поддържа толкова, колкото би могъл да бъде.

Например, ако променим източника на данни, ще трябва да променяме кода на базата данни всеки път, когато използваме App::db в нашето приложение. Освен това не е ясно какво да правим в случай, че искаме да използваме не само информация за текущия потребител?

Опит за тестване на единица

Ето опит за създаване на модулен тест за описаната по-горе функционалност:

Нека да разгледаме този тест. Първо, ще се провали. Променливата $_SESSION, използвана в потребителския клас, не съществува в модулния тест, защото изпълнява кода на командния ред.

Второ, няма установена връзка с база данни. Това означава, че за да работи всичко, ще трябва да стартираме нашето приложение, за да получим обектите App и db. Нуждаем се също от активна връзка с база данни за тестване.

За да работи този модулен тест, трябва да направим следното:

  1. Задайте конфигурационните настройки на нашето приложение за стартиране на интерфейса на командния ред (PHPUnit);
  2. Доверете се на връзката с базата данни. Това означава да третираме източника на данни отделно от нашия тест на единица. Ами ако тестовата ни база данни не съдържа данните, които очакваме? Ами ако имаме бавна връзка с база данни?
  3. Като разчитаме на зареждането на приложението, ние увеличаваме разходите за тестване чрез драстично забавяне на тестовете на единици. В идеалния случай по-голямата част от нашия код трябва да бъде тестван, независимо от използваната рамка.

Е, нека да преминем към това как това може да се подобри.

Спазваме принципа "Не се повтаряй"

Функция, която връща текущия потребител, не е необходима в контекста на нашия прост пример. Това е измислен пример, но следвайки принципа "Не се повтаряй", избирам обобщение на този метод като моя първа оптимизация:

Можем да използваме този метод в нашето приложение. Можем да предадем текущия потребител в извикването на функцията, вместо да инжектираме тази функционалност в нашия модел. Кодът ставапо-модулен и поддържаем, когато вече не зависи от други функционални единици (например глобална променлива на сесия).

Въпреки това, той все още не може да бъде тестван и поддържан толкова лесно, колкото би могъл да бъде. Все още разчитаме на връзка с база данни.

Инжектиране на зависимост

Нека поправим ситуацията, като добавим някои зависимости. Ето как може да изглежда нашият модел, ако поставим връзката с базата данни в клас:

Сега вече са предоставени зависимостите за нашия потребителски модел. Нашият клас вече не предполага конкретна връзка с база данни, нито разчита на глобални обекти.

В момента нашият клас като цяло може да се тества. Можем да предадем източник на данни по наш избор (в по-голямата си част) и потребителски идентификатор и да тестваме резултатите от повикването. Можем също така да превключваме връзки към нашите отделни бази данни (ако приемем, че и двете бази данни прилагат едни и същи методи за извличане на данни). Готино!

Нека да видим как изглежда единичният тест сега:

Добавих нещо ново към този модулен тест: фиктивна реализация. Фиктивната реализация ни позволява да имитираме (фалшиви) PHP обекти. В този случай ние прилагаме фиктивна реализация на връзка с база данни. С нашия "stub" можем да пропуснем тестването на връзката с базата данни и просто да тестваме нашия модел.

Интересувате ли се да научите повече за фиктивното внедряване?

В нашия случай ние симулираме SQL връзка. Уточняваме, че фиктивният обект има методи select, where, limit и get. Връщам самия фиктивен обект, за да отразя как SQL обектът за връзка се връща ( $this ). По този начин извикванията към този метод стават верижни. Забележкаче за метода get връщам резултата от извикването на базата данни - обект от клас stdClass с потребителски данни.

Така се решават няколко проблема:

  1. Тестваме само модела на нашия клас. Ние не проверяваме връзката с базата данни;
  2. Можем да контролираме входовете и изходите на фиктивна връзка с база данни и следователно да извършим стабилно тестване, независимо от резултата от заявката в базата данни. Знам, че ще получа потребителски идентификатор "1" в резултат на извикването на фалшивата база данни;
  3. Не е необходимо да стартираме нашето приложение или да имаме конфигурация или съществуваща база данни, за да тестваме.

Но все още можем да направим нашия код много по-добър. От това място започва най-интересното.

Интерфейси

За по-нататъшно подобрение можем да дефинираме и внедрим интерфейси. Разгледайте следния код:

Тук се случват няколко неща.

  1. Първо, дефинираме интерфейс за нашия източник на данни. Така методът getUser() е дефиниран;
  2. След това внедряваме този интерфейс. В този случай ние правим внедряване на MySQL. Приемаме обект за връзка с базата данни и го използваме, за да изтеглим потребителска информация от базата данни;
  3. И накрая, ние настроихме използването на имплементацията на класа UserRepositoryInterface в нашия потребителски модел. Това гарантира, че източникът на данни винаги ще има наличен метод getUser(), без значение кой източник на данни се използва за внедряване на UserRepositoryInterface.

Обърнете внимание, че нашият потребителски клас обект съдържа въвеждане за обекти на UserRepositoryInterface в своя конструктор. Това означава, че класъткойто имплементира UserRepositoryInterface, трябва да бъде предаден на обект от потребителския клас. Това гарантира, че методът getUser, на който разчитаме, е винаги наличен.

Какво получаваме в резултат?

  • Нашият код вече може да се тества напълно. За потребителския клас ние лесно се подиграваме с източника на данни (тестването на изпълнението на източника на данни е задача на отделен модулен тест);
  • Нашият код стана много по-поддържаем. Можем да свързваме различни източници на данни, без да правим промени в кода в нашето приложение;
  • Можем да създадем ВСЕКИ източник на данни: ArrayUser, MongoDbUser, CouchDbUser, MemoryUser и др.;
  • Можем лесно да предадем всеки източник на данни към нашия потребителски клас обект, ако е необходимо. Ако решите да се отдалечите от SQL базата данни, можете просто да създадете друга реализация (като MongoDbUser ) и да я прехвърлите към вашия потребителски модел.

Ние също така опростихме нашия тест на единица!

Напълно се отървахме от фиктивната връзка с базата данни. Вместо това ние просто се подиграваме на източника на данни и му казваме какво да прави, когато се извика методът getUser.

Но все още можем да направим кода по-добър!

Контейнери

Помислете за използването на текущия ни код:

И това едва ли е в съответствие с принципа "Не се повтаряй". Контейнерите могат да поправят това.

Контейнерът просто съдържа обект или функция. Това е като регистър във вашето приложение. Можем да използваме контейнера, за да създадем автоматично нов потребителски обект с всички необходими зависимости. По-долу използвам общия клас контейнер Pimple.

Преместих създаването на потребителския модел на едно място в конфигурацията на приложението. Като резултат:

  1. Нашият код следва принципа „Не се повтаряй“. Предметна потребителския клас и избраното място за съхранение на данни е дефинирано на едно място в нашето приложение;
  2. Можем да превключим нашия потребителски модел от използване на MySQL към всеки друг източник на данни на ЕДНО място. Това значително опростява поддръжката на кода.

Последна дума

В нашия урок направихме следното:

  1. Подчинихме нашия код на принципа „Не се повтаряй“ и направихме възможно повторното му използване;
  2. Създаден поддържаем код - можем, ако е необходимо, да превключваме източници на данни за нашите обекти за цялото приложение на едно място;
  3. Направи нашия код лесен за тестване - можем просто да се подиграваме на обекти, без да разчитаме на зареждането на нашето приложение или създаването на тестова база данни;
  4. Придобити познания за инжектиране на зависимости и интерфейси, за да се създаде лесен за тестване и поддръжка код;
  5. Видяхме с пример как контейнерите могат да помогнат за по-лесното поддържане на нашето приложение.

Сигурен съм, че сте забелязали, че сме добавили много повече код, за да постигнем лесна поддръжка и възможност за тестване. Силен аргумент срещу подобна реализация е увеличаването на сложността. Всъщност това изисква по-задълбочено познаване на кода, както за главния разработчик, така и за останалите участници в проекта.

Въпреки това, цената на обяснението и разбирането на кода е повече от компенсирана от намаляването на техническия дълг. Кодът стана много по-лесен за поддръжка, с възможността да правите промени само на едно място вместо на няколко.

Възможността за бързо модулно тестване ще намали броя на грешките в кода със значителна разлика - особено в дългосрочни или общностни проекти (с отворен код).

Правенедопълнителна работа сега, ще спестим време и главоболия в бъдеще.

Можете лесно да включите фалшиви обекти и PHPUnit във вашето приложение с помощта на Composer. Добавете това към вашата секция за изискване на разработка във вашия файл composer.json:

След това можете да инсталирате своите базирани на Composer зависимости със следните изисквания:

Когато използвате Laravel 4 PHP рамката, използването на контейнери и други идеи, описани тук, е изключително важно.

Благодаря за четенето!

Тази публикация е превод на статията " Как да напишем код, който може да се тества и поддържа в PHP ", изготвена от приятелския екип на проекта Internet Technologies.ru