Статьи

Предотвращение кода Rot 101: модульное тестирование

Один мудрый программист однажды сказал: «Когда я фиксирую свой код, только Бог и я знаем, что он делает. Через некоторое время только Бог знает ». Это базовое определение гнили кода, которое применимо почти ко всему коду, который мы пишем.

Конечно, код все равно будет делать то же, что и когда вы его зарегистрировали или когда отправили в производство, но, поскольку он работает, он становится чем-то волшебным: проверенная правда, и вы не трогаете проверенные истины. Со временем код превращается в чудовище, которого никто не осмеливается трогать, унаследованную часть кодовой базы. Он был разработан «давным-давно», он функционировал точно так же с момента своего выпуска, и никто не понимает его полностью. Если что-то нужно изменить, вы взломаете это; Вы изменяете вход или выход, но оставляете черный ящик нетронутым.

Если вы сейчас не работаете над новым проектом, кодовая база, с которой вы работаете, скорее всего, содержит такой код, который вы воспринимаете как должное. И чем сложнее ваше текущее задание (или новый проект), тем больше вероятность того, что оно станет неотъемлемой частью в короткие сроки.

Поскольку невозможно полностью понять все проблемы, которые решают ваши кодовые базы, а также все их решения, необходимо предпринять что-то еще, чтобы предотвратить гниение кода. Так или иначе: код должен быть рефакторируемым, чтобы жить долго и счастливо. И вот тут приходят юнит-тесты; они предоставляют простой способ написать доказательство того, что ваш код работает так, как задумано. И каждое доказательство делает то, что доказано, рефакторимо. Вы можете изменить проверенный код, а затем попросить тесты доказать, работает ли новый код так же.

Хотя я хотел бы рассказать вам каждую мелочь, которую я знаю о модульном тестировании и о том, как применить его к вашему коду, я неохотно согласился с моим редактором, что 150 000 слов слишком длинны для одной статьи. Так что я придерживаюсь основ.

Основы модульного тестирования

В принципе, модульное тестирование — это небольшой фрагмент кода, который выполняет 3 вещи:

  • Assemble — настраивает среду, аналогичную той, в которой код будет выполняться во время производства, но без зависимости от других компонентов в вашем коде
  • Act — выполняет часть кода, которая будет доказана
  • Утверждает — проверяет, что результат казни соответствует ожидаемому с учетом обстоятельств

Проницательный читатель заметит, что это не проверяет код на правильность. Если в коде допущена ошибка, допущенная тем же разработчиком, который написал модульный тест, ошибка не будет найдена, поскольку тест будет написан с теми же предположениями. Ошибки, вызванные опечатками, будут обнаружены при запуске тестов, подобно тому, как они будут обнаружены, если код был выполнен в промежуточной среде. Таким образом, модульное тестирование — это немного вводящее в заблуждение название: оно больше проверяет юнит, чем юнит-тестирование

Проверяемая единица зависит от разработчика, пишущего юнит-тест, хотя эмпирическое правило в этом заключается в том, что единица должна действовать, то есть должна иметь поведение. Простые классы сущностей, которые хранят только значения, не нуждаются в модульных тестах, поскольку доказывать нечего. Вы можете положиться на язык программирования, чтобы иметь возможность хранить значения в переменной; нет смысла проверять это.

На связанной ноте, не стесняйтесь игнорировать номера покрытия кода. Это бесполезные показатели. Единственное, что имеет значение, — есть ли у действующих компонентов или узлов доказательства. У вас может быть колоссальное покрытие кода 95%, но если эти 5% составляют основную бизнес-логику, у вас все еще есть (будущие) черные ящики, ожидающие начала гниения.

собирать

Важной частью этапа сборки является то, что среда имитирует производство, но не использует какие-либо компоненты / узлы, не прошедшие тестирование. Если вам нужно доказать метод, который изменяет значение, полученное из базы данных, упростите его до простой модификации значения. Вы можете смоделировать компонент базы данных, который обеспечит ценность в производстве, если ваш код тесно связан.

С помощью фиктивного компонента вы создаете фиктивный компонент, который для тестируемого компонента представляется как реальный компонент, но будет вести себя предсказуемо. Скажем, тестируемый компонент получает список имен из базы данных через компонент уровня данных, а затем использует заглавные буквы для каждого первого символа. Поддельный объект имитирует компонент уровня данных, но фактически не взаимодействует с базой данных. Вместо этого он содержит статический список имен.

<?php
...
    // ** ORIGINAL METHOD **/
    public function getCustomerFirstNames(DBLayer $db) {
        $dbvalues = $db->getQueryResults("SELECT firstname FROM customers");
        $firstnames = array();
        foreach($dbvalues as $firstname) {
            $firstnames[] = ucfirst($firstname);
        }
        return $firstnames;
    }
...
 <?php
//** Mock for DB layer **/
class DBLayer
{
    public function getQueryResults($sql) {
        return array("John", "tim", "bob", "Martin");
    }
}
 <?php
...
    //**PHPUnit testcase **/
    public function setUp() {
        //mock file included instead of real file
        $this->db = new DBLayer();
    }
 
    public function testGetcustomerfirstnamesWillReturnFirstnamesModifiedByUcfirst() {
        // assemble (together with the runonce setUp() method)
        $obj = new OriginalClass();
        $expected = array("John", "Tim", "Bob", "Martin");
        // act
        $results = $obj->getCustomerFirstNames($this->db);
        // assert
        $this->assertEquals($expected, $results, "GetCustomerFirstNames did not ucfirst() the list of names correctly");
    }
...

При таком подходе были достигнуты две вещи:

  • Тестовый компонент не зависит от других компонентов.
  • Собранная среда, в которой мы запускаем тест, предсказуема; он будет вести себя одинаково при каждом запуске теста

Действовать и утверждать

При наличии подходящей среды можно выполнить тестовый компонент. Учитывая, что входные данные известны, а также какую проблему должен решить код и как он решается, результат может быть легко получен. И это именно то, для чего предназначена стадия утверждения. Это та часть, которая на самом деле доказывает код.

Утверждение просто гласит: «при известном вводе x, обработанном известной функцией f, выводом является f (x)». Разработчик, который написал функцию f и сопутствующий модульный тест, написал эту функцию, чтобы решить конкретную проблему под рукой. Тест устраняет необходимость знать или даже понимать проблему, или почему она была решена определенным образом. Функциональность кода была принята (и, надеюсь, постоянно принимается приемочными тестами), так что пока проходит модульное тестирование; код подтвержден

Это означает, что теперь код можно реорганизовать столько раз, сколько необходимо, с помощью модульного теста, служащего доказательством того, что функциональность осталась неизменной. Это устраняет основную причину гниения кода, так как код теперь можно безопасно изменять. Это не может быть сломано, пока это покрыто модульными тестами, которые проходят. Если тест не пройден, разработчик быстро уведомляется и может скорректировать или отменить примененное изменение даже до того, как код будет выполнен в промежуточной среде.

Прикладное юнит-тестирование

Теперь, когда у вас есть понимание того, что такое модульные тесты и как они работают, я хотел бы коснуться нескольких моментов, которые значительно улучшат ваш опыт модульного тестирования.

Модульные тесты не являются интеграционными тестами, приемочными тестами или любой другой формой тестов реализации.
В то время как каждая инфраструктура модульного тестирования предоставит вам полный набор инструментов, что делает заманчивым пробраться в некоторые интеграционные или приемочные тесты … не делайте этого. Модульные тесты должны только доказать, что решение оригинального разработчика все еще применяется, несмотря на любые изменения в тестируемом коде. Хотя вы можете написать интеграционные или приемочные тесты с одной и той же платформой, вы можете рассмотреть возможность использования двух отдельных платформ, чтобы их было проще разделить.

Не забудьте также написать интеграционные / приемочные тесты.
Модульное тестирование показывает крошечный фрагмент кода изолированно, но не доказывает, что система работает. Это покрыто интеграционными и приемочными тестами; набор тестов, который работает медленно, объединяет все компоненты и доказывает, что части работают вместе, как они должны. Модульный тест является лаконичным и быстрым, что позволяет проводить его до и после каждого изменения, не тратя при этом много времени. Интеграционный / приемочный тест — это то, что вы запускаете в конце цикла разработки, на этапе QA.

Сделайте свои юнит-тесты простыми и краткими.
Имейте в виду, что модульные тесты будут помечены как провальные, если хотя бы одно утверждение не выполнено, поэтому следует придерживаться общего практического правила: чем меньше утверждений в тесте, тем лучше. Хороший тест никогда не смешивает доказательства поведения. Если такой тест не пройден, вы сразу же точно знаете, какая функциональность нарушена. Если доказательства поведения смешиваются или в одном и том же тесте присутствуют несколько утверждений, вам нужно запустить отладчик, чтобы выяснить, какое поведение теперь нарушено. Все утверждения могут быть ошибочными, или только одно, только первое ошибочное утверждение будет сообщено.

Назовите свои юнит-тесты многословно.
Имя метода тестирования никогда не будет вызываться вручную, поэтому нет необходимости использовать умные или краткие соглашения об именах. Детальность в названии служит дополнительной формой документации для того, что проверяется.

Дополнительным бонусом является то, что при выборе вывода в формате TestDox инфраструктура модульного тестирования, такая как PHPUnit, создает хороший контрольный список с отображаемыми человеком читаемыми строками, который служит документацией . Если вы пишете свои тесты до фактической реализации, вы даже можете использовать этот вывод в качестве быстрого контрольного списка, чтобы увидеть, что закончено и что еще нужно написать.

Пишите только модульные тесты для собственного кода.
В то время как фреймворки для модульного тестирования дают вам так много инструментов, что становится заманчиво писать тесты для всего, что есть в поле зрения, только код, который вы написали лично, может быть надлежащим образом протестирован. Хороший код — это решение проблемы; это результат глубокого понимания проблемы, понимания возможных решений и, наконец, фактической реализации решения. Если видна только реализация, тест будет написан без тех же предположений и понимания исходного решения проблемы. В этом случае весьма вероятно, что в модульном тесте будут пропущены конкретные варианты проектирования, что сделает тест ненадежным.

Напишите модульные тесты для исправления ошибок.
Перед исправлением ошибки напишите тест, который подтверждает, что текущий код неверен (потому что утверждение не выполняется). Затем исправьте код, чтобы неизменное утверждение прошло. В качестве побочного эффекта, это отличный способ начать добавлять модульные тесты в открытый на данный момент фрагмент кода. Это помогает вылечить гниль кода, один маленький модульный тест за раз, поскольку эта часть кода теперь доказана, чтобы быть правильной (для текущего определения правильного). Это также предотвращает повторное внесение ошибки при рефакторинге, так как повторное включение ошибочного поведения приведет к сбою модульного теста.

Никогда не меняйте тестируемый код.
Когда вы начнете писать тесты для уже написанного кода, вы быстро заметите, что большую часть кода сложно протестировать из-за тесной связи или других вариантов дизайна. Будет очень заманчиво немного изменить тестируемый код, чтобы было проще написать для него модульный тест, но не делайте этого. Вся цель модульного тестирования состоит в том, чтобы доказать существующий код, чтобы его можно было безопасно реорганизовать. Рефакторинг перед написанием модульного теста — это именно тот риск, которого вы хотите избежать — вы не будете знать, что сломаете или какие ошибки вы внесете.

Округления

Менеджеры не собираются назначать полную команду для каждой проблемы и заставляют их поддерживать эту крошечную кодовую базу на протяжении всей вечности, поэтому, если вы хотите предотвратить гниение ваших гениальных решений, вы пишете своих собственных маленьких виртуальных помощников в форме модульные тесты, чтобы поддерживать эту кодовую базу для вас.

Если вы гордитесь кодом, который вы пишете, вы должны иметь для него модульные тесты. Если вы не знаете, как писать модульные тесты, вы должны научиться их писать. Если ваша команда не использует модульные тесты, убедите их, что это необходимый инструмент.

Намного больше нужно узнать о модульных тестах и ​​о том, почему вы должны их писать; для дальнейшего чтения посетите:

Не стесняйтесь спрашивать в своей локальной группе пользователей PHP или посещать сессии на эту тему на следующей конференции. Большинство профессиональных разработчиков PHP имеют некоторый опыт работы с PHPUnit и модульным тестированием в целом.

Более мудрый программист сказал бы: «Когда я фиксирую свой код, только Бог выполняет модульные тесты, и я знаю, что он делает. Через некоторое время об этом узнают только Бог и юнит-тесты ».

Изображение через Fotolia