Стратегии реструктуризации непроверяемого PHP-кода

Модульное тестирование и реструктуризация унаследованного PHP-кода для облегчения проверки и повышения качества кода

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

Джон Мертик, инженер-программист, SugarCRM

Джон Мертик (John Mertic) получил диплом инженера по вычислительной технике в университете Kent State University и в настоящее время работает инженером-программистом в компании SugarCRM. Он участвовал во многих проектах open source, главным образом в РНР-проектах; является создателем и хранителем PHP Windows Installer.



28.09.2011

Введение

Оглядываясь на 15-летнюю историю развития PHP, мы видим, как из простого динамического языка сценариев, альтернативного сценариям CGI, которые были популярны в то время, вырос полноценный язык программирования, каким PHP является сегодня. По мере роста базы кода ручное тестирование становится невыполнимой задачей, и каждое изменение в коде, крупное или мелкое, может повлиять на все приложение. Это может быть всего лишь страница, которая не загружается, или форма, которая не сохраняется, но может быть и ошибка, которую трудно обнаружить или которая проявляется только при определенных условиях. А то и привести к возвращению, казалось бы, уже устраненной ранее ошибки приложения. Для решения этих проблем разработаны различные инструменты тестирования.

Один популярный метод, называемый функциональными или приемо-сдаточными испытаниями, заключается в том, что приложение проверяют в процессе типичного взаимодействия с пользователем. Это хороший метод для тестирования различных процессов приложения, но он может оказаться очень медленным и в общем случае плохо проверяет низкоуровневые классы и функции. Для этого больше подходит другой метод, метод модульного тестирования. Цель состоит в том, чтобы проверить работоспособность внутреннего кода приложения и гарантировать, что при его выполнении получаются правильные результаты. "Зрелые" Web-приложения часто содержат много унаследованного кода, который со временем становится трудно проверить, а это сокращает возможности команды разработчиков по обеспечению хорошего охвата при тестировании приложений. Обычно такой код называют "непроверяемым". Давайте посмотрим, как можно выявить его в своем приложении и исправить.


Выявление непроверяемого кода

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


Функции, зависящие от глобального состояния

Глобальные переменные в PHP-приложениях ― это большое удобство. Они позволяют работать с переменными или объектами, которые можно инициализировать в начале, а затем использовать в любом месте приложения. Но за эту гибкость приходится платить, так как интенсивное использование глобальных переменных часто приводит к непроверяемому коду. Это видно на примере листинга 1.

Листинг 1. Функция, зависящая от глобального состояния
<?php 
function formatNumber($number) 
{ 
    global $decimal_precision, $decimal_separator, $thousands_separator; 

    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 

    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator); 
}

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

Листинг 2. Та же функция, исправленная так, чтобы глобальные переменные можно было переопределять
<?php 
function formatNumber($number, $decimal_precision = null, $decimal_separator = null, 
$thousands_separator = null) 
{ 
    if ( is_null($decimal_precision) ) global $decimal_precision; 
    if ( is_null($decimal_separator) ) global $decimal_separator; 
    if ( is_null($thousands_separator) ) global $thousands_separator; 

    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 

    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator);
}

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


Синглтоны, не приводимые в исходное состояние

Синглтоны – это классы, которые в приложении в каждый момент времени могут иметь только один экземпляр. Обычно они применяются в приложении в качестве глобальных объектов, таких как соединения с базой данных и параметры настройки. Их часто запрещают применять в приложении, что многие разработчики считают несправедливым ввиду полезности иметь в своем распоряжении всегда доступный для использования объект. Во многом это следствие злоупотребления синглтонами, когда многие из этих так называемых объектов бога бывает невозможно расширить. Но с точки зрения тестирования главная проблема состоит в том, что они часто неизменяемы. Рассмотрим, например, листинг 3.

Листинг 3. Объект-синглтон, который нужно протестировать
<?php 
class Singleton 
{ 
    private static $instance; 

    protected function __construct() { } 
    private final function __clone() {} 
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 

        return self::$instance; 
    } 
}

Как видите, после появления первого экземпляра синглтона каждый вызов метода getInstance() будет возвращать один и тот же объект, вместо нового, что может стать большой проблемой при внесении изменений в этот объект. Простейшее решение ― добавить к объекту метод, который будет его сбрасывать. Пример приведен в листинге 4.

Листинг 4. Объект-синглтон с добавленным методом сброса
<?php 
class Singleton 
{ 
    private static $instance; 

    protected function __construct() { } 
    private final function __clone() {} 
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 

        return self::$instance; 
    } 

    public static function reset() 
    { 
        self::$instance = null; 
    } 
}

Теперь при запуске каждого теста можно вызвать метод сброса, чтобы гарантировать прохождение кода инициализации объекта-синглтона при каждом прогоне теста. Этот метод может быть полезен и для приложения в целом, потому что теперь синглтон становится легко изменяемым.


Работа в конструкторе классов

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

Листинг 5. Класс с тяжелым методом-синглтоном
<?php 
class MyClass 
{ 
    protected $results; 

    public function __construct() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable'); 
    } 

    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

Здесь, чтобы протестировать метод fdfdfd в объекте, нам в конечном итоге приходится установить соединение с базой данных, внести записи в таблицу, а затем очистить все эти ресурсы после тестирования. Это кажется излишним, если учесть, что для проверки метода fdfdfd ничего этого не нужно. Давайте изменим конструктор, как показано в листинге 6.

Листинг 6. Класс изменен так, чтобы можно было опустить всю ненужную логику инициализации
<?php 
class MyClass 
{ 
    protected $results; 

    public function __construct($init = true) 
    { 
        if ( $init ) $this->init(); 
    } 

    public function init() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable');
    } 

    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

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


Наличие жестких зависимостей классов

Как видно из предыдущего раздела, огромная проблема конструкции классов, которая затрудняет тестирование, заключается в необходимости инициализировать разнообразные объекты, которые не нужны для вашего теста. Мы уже видели, как тяжелая логика инициализации может добавлять всевозможные накладные расходы в процесс написания тестов (особенно, когда для успеха теста ничего этого не требуется), но еще одна проблема может возникнуть при непосредственном создании новых объектов внутри методов тестируемого класса. Рассмотрим, например, листинг 7.

Листинг 7. Класс с методом, который непосредственно инициализирует другой объект
<?php 
class MyUserClass 
{ 
    public function getUserList() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $results = $dbconn->query('select name from user'); 

        sort($results); 

        return $results; 
    } 
}

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

Листинг 8. Класс с методом, который непосредственно инициализирует другой объект, но позволяет заменить его
<?php 
class MyUserClass 
{ 
    public function getUserList($dbconn = null) 
    { 
        if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) { 
            $dbconn = new DatabaseConnection('localhost','user','password'); 
        } 
        $results = $dbconn->query('select name from user'); 

        sort($results); 

        return $results; 
    } 
}

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


Полезность проверяемого кода

Конечно, более проверяемый код имеет очевидное преимущество, позволяя легче писать модульные тесты для PHP-приложения (это видно на примерах, приведенных в этой статье), но при этом еще и создается лучше спроектированное, более модульное и более стабильное приложение. Все мы видели "спагетти"-код разного уровня, тесно переплетенный с бизнес-логикой и логикой отображения в один большой процедурный ком, приводящий к созданию PHP-приложений, которые, несомненно, вызовут большие проблемы у тех, кому придется углубляться в этот код в целях поддержки. Чтобы сделать его проверяемым, мы реструктурировали ранее проблематичный код; проблематичный не только по своей структуре, но и по своей функции. Мы открыли возможности для лучшего многократного использования этого кода; удалив жесткие зависимости, мы сделали функции и классы менее одноцелевыми и более удобным для использования в других местах приложения. Кроме того, мы облегчили будущую поддержку базы кода, исключив низкопробный код и заменив его более высококачественным.


Заключение

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


Загрузка

ОписаниеИмяРазмер
Исходный код для статьиcode_examples.zip3 КБ

Ресурсы

  • Оригинал статьи.(EN)
  • Серия пособий developerWorks Learning PHP ― от самых простых сценариев PHP до работы с базами данных и организации потоков из файловой системы.(EN)
  • PHP.net ― центральный ресурс для PHP-разработчиков.(EN)
  • Раздел PHP на developerWorks.(EN)
  • Руководство по переходу на PHP V5 (Джек Херрингтон, DeveloperWorks, сентябрь 2006 г.): как перенести код, разработанный в PHP V4, на V5 (EN).
  • Планета PHP – источник новостей сообщества PHP-разработчиков. (EN)

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, XML
ArticleID=762244
ArticleTitle=Стратегии реструктуризации непроверяемого PHP-кода
publish-date=09282011