02.02.2012

Автоматизируем тестирование

Вчера я побывал на встрече участников Московского клуба тестировщиков и просто людей, неравнодушных к тестированию. Встреча проходила в новом офисе Афиши-Рамблера. Здесь я попробую поделиться услышанным.

Во-первых, это было мое первое знакомство с такими специалистами в тестировании, как Алексей Баранцев и Наталья Руколь. Очень понравился и Алексей, и Наталься, и шляпа в которой была Наталья :-)

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

Итак, новому участнику в проекте (представьте, что это вы) наверняка захочется сделать тестирование более оптимальным: ручное тестирование заменить автоматизированным. Чтобы за одно и тоже время можно было запустить больше прогонов с большим набором тестовых данных и т.д. Особенно актуальны эти намерения для тестировщика, который автоматизацией уже занимался ранее.

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

Далее следует заняться составлением стратегического плана. При этом нужно использовать информацию, которую вы получили при обосновании. Нужно решить, что можно поменять, а что нет. Решить, что можно поменять, но не сразу. Очень сокращенный пример такого плана быть таким: «за год сократить время выполнения тестов в 2 раза без снижения полноты покрытия». Или таким: «за год увеличить покрытия кода тестами с 40 до 80 процентов а счет увеличения количества негативных тестов без увеличения времени выполнения».

Далее, собственно идет стадия реализация вашей стратегии. При этом есть пять разновидностей ваших действий:
  • выжидание – вы тратите какое-то небольшое время, возможно случайно образовавшееся. Например, если заболел ведущий разработчик, то у тестировщиков наверняка будет меньше работы. Это время стоит использовать с пользой, например, подготовить один из тестов.
  • прорыв – вы определенными, не самыми сложными действиями, получаете сравнительно легкий и эффектный результат. Пример прорыва – это использование рекордера тестов.
  • давление – вы посвящаете автоматизации каждый день один-два часа, а то и больше. Или, например, автоматизируете тест любого бага, который проявился дважды.
  • массированный удар – до достижения запланированного уровня автоматизации осталось уже совсем немного, и вы начинаете тратить уже как минимум половину рабочего времени на автоматизацию, чтобы быстрее все «добить».
  • и последняя стадия – это удержание, когда приоритетом для вас является защита и реагирование. Здесь например, вы принимаете решение сразу автоматизировать тесты.
Таким образом, последовательность осуществления плана по автоматизации: ожидание, прорыв, давление, массированный удар, удержание и т.д.

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

13.11.2011

Eclipse PDT: начало работы

Разбирая свои старые файлы, я наткнулся на черновик статьи, которую я так и не опубликовал. В материале речь идет об установке Eclipse для работы с PHP на удаленном сервере. Ранее я уже приводил решение для комфортной работы с Eclipse PDT по SSH. Здесь же будет небольшой гид, который должен быть полезен прежде всего тем, кто совсем немного работал с данной IDE.

Выбор дистрибутива
Прежде всего нужно выбрать подходящий дистрибутив Eclipse. Разработку для PHP в Eclipse можно вести благодаря проекту PHP Developer Tools (PDT). Чтобы загрузить дистрибутив IDE можно выбрать одну из опций, которые доступны на текущий момент:
  • скачать Eclipse 3.7 Classic и через меню Install new Software установить пакет "PDT Development Tools All in One SDK";
  • скачать Eclipse 3.6 для PHP-разработчиков (т.е. предыдущую версию IDE, но специальную сборку для PHP);
  • найти на странице PDT ссылки для загрузки Eclipse 3.7 для PHP-разработчиков (такие сборки у проекта PDT появились сравнительно недавно).

Начало работы
Eclipse не нужно устанавливать. Достаточно просто разместить файлы в директорию, например "C:\eclipse" для пользователей Windows.

После этого можно просто запустить "eclipse.exe". Среда разработки начнет запускаться и предложит выбрать путь до вашего рабочего каталога "Workspace". Можно выбрать примерно такой же путь, как на рисунке ниже: "С:\Users\username\workspace".


Теперь Eclipse должен запуститься. Начальный вид окна может быть различным, но нам, PHP разработчикам, интересна перспектива PHP (это настройки и вид среды разработки). Открыть перспективу PHP можно способом, показанным на рисунке ниже: выполнить команды меню "Window => Open perspective => PHP (или Other…, если PHP нет в списке)".


Редактирование удаленных файлов
В нашей работе довольно распространена необходимость редактировать файлы, расположенные на удаленных серверах. Eclipse PDT изначально позволяет работать в основном через проекты. Для удаленных файлов – удаленные проекты. Это не совсем удобно, и было бы лучше иметь возможность простого редактирования файлов, неважно локальных или удаленных.

Для этого необходимо установить расширение "Target Management" или по-другому его еще называют "Remote Systems". Для установки расширения необходимо отрыть меню "Help => Install New Software". Далее в окне выбора пакетов для установки необходимо найти "Remote System Explorer End-User-Runtime", обычно пакет находится в разделе "General Purpose Tools". После перезапуска в перспективу PHP следует добавить окно из только что установленного дополнения. Для этого следует выполнить команды: "Window => Show View => Other…".


В списке следует найти и выбрать "Remote Systems", как на рисунке ниже:


Новое окно для удаленного редактирования, скорее всего, появится внизу экрана. Окно удобнее всего перетащить в левую часть, так с ним будет удобно и привычно работать. Теперь окно «Remote Systems» находится в привычном месте, это основное окно, в котором можно открывать удаленные или локальные файлы.

Создание соединения с сервером
Теперь попробуем установить соединение с удаленным сервером . Первое, что нужно сделать, это кликнуть правой кнопкой в пустой области окна "Remote Systems" и выбрать там: "New => Connection…". Далее из списка следует выбрать тип соединения "SSH Only".


Теперь нужно указать параметры соединения, достаточно ввести только Host Name, например "example.hostname.ru". Соединение создано, теперь можно попробовать подключиться. Для этого следует открыть одну из папок на удаленном сервере, например "My Home". Eclipse должен запросить у вас параметры учетной записи: логин и пароль. Если все параметры были указаны верно, теперь есть возможность легко открывать и редактировать удаленные файлы.

Кроме того, в Remote Systems можно без проблем открывать и локальные файлы, как это показано на рисунке ниже:


Автодополнение и временные файлы
Несмотря на то, что мы использовали готовый дистрибутив для PHP разработчиков или установили пакет PDT All-in-One, есть некоторые проблемы при редактировании удаленных файлов. В частности, плохо работает автодополнение.

Этот вопрос уже изучался, и было найдено некоторое решение проблемы. Оказалось, что для удаленного редактирования файлов через Remote Systems используется специальный проект. Если этот проект преобразовать в PHP-проект, то проблем с автодополнением не должно быть.

Фактически нужно выполнить следующие действия:
  1. Необходимо закрыть Eclipse;
  2. В проводнике или в другом файловом менеджере, например Nautilus’е, открыть рабочий каталог Workspace, для меня это каталог "С:\Users\hamuhin\workspace";
  3. В каталоге Workspace должна быть папка "RemoteSystemsTempFiles", - это и есть проект, который нужно преобразовать;
  4. В папку RemoteSystemsTempFiles необходимо скопировать файлы ".buildpath: и ".project", скачать их можно здесь. Если файлы уже существуют в каталоге, то их нужно переименовать в бэкап, а новые файлы разместить на их место;
  5. Это почти все, теперь можно снова запустить Eclipse.
Проверим, что автодополнение работает. Для этого откроем любой файл на сервере webdev и попробуем ввести код $abc = str и нажать Ctrl+Пробел. Должен появиться список функций, как на рисунке ниже:


В процессе работы на диске может скопиться очень много файлов, которые когда то открывались на удаленных серверах. Со временем это может сильно замедлить работу Eclipse. Чтобы этого избежать, нужно ограничить количество временных файлов. Для этого необходимо открыть настройки Eclipse: "Window => Preferences".

В окне настроек следует найти раздел "Remote Systems / File Cache" и там включить опцию Limit cache size. Оптимальное значение может зависеть от объема оперативной памяти на машине, я обычно устанавливаю 512 или 256 МБ.

Настройки Proxy
Если Eclipse не может соединиться с сервером, то эти проблемы с сетью могут быть связаны с настройками Proxy. Для того чтобы проверить или изменить эти параметры следует открыть окно настроек Eclipse: "Window => Preferences". В фильтре можно написать "proxy", чтобы быстро найти нужный раздел.

Фильтры и скрытые файлы в Remote Systems
Часто необходимо редактировать файлы, которые находятся, например, по такому длинному пути: "/var/www/my-big-project/htdocs". Чтобы каждый раз не выбирать последовательно эти каталоги в окне Remote Systems, можно создать фильтр, который позволит быстро открывать нужные файлы.

Для создания фильтра на интересуещем каталоге, например "my-big-project", нужно кликнуть правой кнопкой и выбрать из меню: "New => Filter…". Далее можно следовать инструкциям, ничего не меняя, и только на заключительном этапе указать имя фильтра, например «my-big-project». Теперь к файлам можно обращаться по более короткому пути, с помощью нового фильтра.

Кроме того, часто бывает нужно отредактировать скрытые файлы, например ".htaccess". Обычно скрытые файлы с точкой вначале не отображаются в Remote Systems. Чтобы изменить эту опцию, нужно открыть окно настроек "Remote Systems / Files" и установить галочку в опции "Show hidden files".

Работа с разными кодировками
В настройках Eclipse можно устанавливать кодировку по умолчанию. Однако бывает так, что один проект создается в UTF-8, а другие ведутся в CP1251. Каждый раз менять настройку кодировки не очень удобно. К счастью можно задавать кодировку только для отдельных проектов, файлов или каталогов. Чтобы изменить кодировку для каталога, необходимо кликнуть правой кнопкой по нему и выбрать «Properties». В окне свойств каталога можно установить нужную кодировку, например UTF-8.

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

05.11.2011

Обработка исключительных ситуаций

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

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

Если мы решили сделать исключительную ситуацию непроверяемой (unchecked exception), то нам следует разместить блок проверки для возможного незакрытого счета покупателя в месте вызова метода closeAccount. Таким образом за проверку условия отвечает вызывающий.
<?php 
class Customer
{
    public function 
closeAccount()
    {
        if (!empty(
$this->uncompletePayments))
        {
            throw new 
Exception("Cannot close customer account with uncomplete payments");
        }
        
        
$this->accountClosed true;
    }
}
?>
<?php 
if (!$customer->canClose())
{
    
$this->handleCloseError();
} else
{
    
$customer->closeAccount();
    
//do some other usual things
}
?>
В данном случае невыполнение условия в методе closeAccount будет являться ошибкой программирования (возлагаемая на вызывающего обязанность не была правильно выполнена).

В случае, если мы решили сделать исключительную ситуацию проверяемой (checked exception), то нам следует разместить вызов метода в блоке try.
<?php
class CustomerCloseException
    
extends Exception
{
    
}

class 
Customer
{
    
/**
     * Some description
     * 
     * @throws CustomerCloseException
     */
    
public function closeAccount()
    {
        if (!empty(
$this->uncompletePayments))
        {
            throw new 
CustomerCloseException();
        }
        
        
$this->accountClosed true;
    }
}
?>
<?php
try
{
    
$customet->closeAccount();
    
//do some usual things
    
} catch (CustomerCloseException $e)
{
    
$this->handleCloseError();
}
?>
Здесь необходимо явно сообщить о возможной исключительной ситуации в интерфейсе (в данном случае я это делаю с помощью тэга phpdoc @exception или @throws). Так вызывающий может понять, что ему следует ожидать.

01.11.2011

Новое в PHPUnit 3.6

Несколько дней назад вышла новая версия фреймворка для модульного тестирования PHPUnit 3.6. Попробуем разобраться, какие нововведения вошли в этот релиз.

Одно из нововведений - это возможность пользоваться методом returnSelf для mock-объектов. Ниже приведу пример, похожий на пример в документации:
<?php
class SomeClass
{
    public function 
doSomething()
    {
        
//do something and return null
        
return null;
    }
}

class 
ReturnSelfTest extends PHPUnit_Framework_TestCase
{
    public function 
testReturnSelfExample()
    {
        
//GIVEN
        //create a stub for the SomeClass class
        
$stub $this->getMock('SomeClass');

        
//configure the stub
        
$stub->expects($this->any())
             ->
method('doSomething')
             ->
will($this->returnSelf());

        
//WHEN
        
$result $stub->doSomething();

        
//THEN
        
$this->assertSame($stub$result);
    }
}
?>
Другой новый метод для Mock-объектов называется returnValueMap. Метод позволяет указывать какое должно быть возвращаемое значение в зависимости от аргументов. Попробуем разобраться на примере:
<?php
class SomeClass
{
    public function 
doSomething()
    {
        
//do something and return null
        
return null;
    }
}

class 
ReturnValueMapTest extends PHPUnit_Framework_TestCase
{
    public function 
testReturnValueMapExample()
    {
        
//GIVEN
        //create a stub for the SomeClass class
        
$stub $this->getMock('SomeClass');

        
//create a map of arguments to return values.
        
$map = array(
          array(
'Fry''Bender''ExampleResultFirst',),
          array(
'Lisa''Bart''Homer''ExampleResultSecond')
        );

        
//configure the stub
        
$stub->expects($this->any())
             ->
method('doSomething')
             ->
will($this->returnValueMap($map));

        
//WHEN
        
$resultFirst $stub->doSomething('Fry''Bender');
        
$resultSecond $stub->doSomething('Lisa''Bart''Homer');

        
//THEN
        //the provided arguments
        
$this->assertEquals('ExampleResultFirst'$resultFirst);
        
$this->assertEquals('ExampleResultSecond'$resultSecond);
    }
}
?>
Следующее нововведение - это директива @requires для анотации тестовых методов:
<?php
class RequiresTest extends PHPUnit_Framework_TestCase
{
    
/**
     * @requires PHP 6.0
     */
    
public function testRequiresExample()
    {
        
//test something
    
}
}
?>
Выполнение теста будет выглядеть таким образом:
phpunit -v

PHPUnit 3.6.0 by Sebastian Bergmann.

Configuration read from /home/username/application/tests/phpunit.xml

S

Time: 0 seconds, Memory: 4.25Mb

There was 1 skipped test:

1) ArticleTest::testRequiresExample
PHP 6.0 (or later) is required.

OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Incomplete: 0, Skipped: 1.
Новое утверждение assertCount (и противоположное assertNotCount) для проверки количества элементов в массиве. И еще несколько нововведений коротко:
  • появилась возможность выводить результаты code coverage в текстовом виде;
  • еще одна новая анотация @testdox, очевидно, для генерации документации для выбранных методов;
  • режим strict, в котором тесты без утверждений считаются незавершенными;
  • и еще несколько опций запуска phpunit (узнать о них лучше всего из документации): coverage-php и printer.
Пользуясь случаем, поздравляю данный фреймворк, а точнее его создателей, со скорой круглой датой: первая версия PHPUnit вышла 27 ноября 2001 года, т.е. уже почти 10 лет назад!

30.10.2011

Незаменимые инструменты: PHP_CodeSniffer, PHP Depend и PHPMD

В прошлом посте, мы начали обсуждать небольшие утилиты, которые, находясь всегда под рукой, способны помочь разработчикам находить в исходном коде проблемные места, позволяют взглянуть на код с другой стороны, и в общем случае помогают быстрее выполнять свои задачи. Сегодня эта тема будет продолжена. На этот раз я бы хотел обсудить три таких инструмента:
Сначала мы поговорим о PHP_CodeSniffer. Этот инструмент позволяет “понюхать” ваш код на дурные запахи. CodeSniffer незаменим, если вы хотите использовать такую инженерную практику, как code review. Не стоит самостоятельно проверять код на соблюдение принятого code style. Эту работу лучше сделает CodeSniffer. Запустить проверку можно таким способом:
phpcs --standard=PEAR application/classes

FILE: application/classes/legacy.class.php
------------------------------------------------------------------------
FOUND 4 ERROR(S) AND 1 WARNING(S) AFFECTING 5 LINE(S)
------------------------------------------------------------------------
  2 | ERROR   | Missing file doc comment
  3 | ERROR   | Missing class doc comment
  6 | ERROR   | Missing function doc comment
 11 | ERROR   | You must use "/**" style comments for a function comment
 14 | WARNING | Line exceeds 85 characters; contains 89 characters
------------------------------------------------------------------------

Time: 0 seconds, Memory: 3.25Mb
Безусловно существует возможность настроить правила проверки под свои стандарты кодирования. Для этого можно обратиться к документации, где рассмотрен один простой пример. В этом месте я бы хотел остановиться немного дольше, чем при рассмотрение других инструментов, и показать другой пример для создания своего правила проверки стиля написания кода в PHP_CodeSniffer.

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

Итак, для начала, мы создадим свой стандарт кодирования, разместив свой каталог в иерархию PHP_CodeSniffer. В моем случае это такой путь: /usr/share/php/PHP/CodeSniffer/Standards/Example. Здесь мы должны разместить файл ruleset.xml со следующим содержанием. См. синтаксис файлов ruleset.
<?xml version="1.0"?>
<ruleset name="Example">
    <description>The Example coding standard.</description>
    <rule ref="PEAR" />
</ruleset>
Здесь мы позаимствовали все стандарты PEAR, а свой стандарт для проверки использования венгерской нотации для переменных мы поместим в файл Sniffs/NamingConventions/HungarianNotationSniff.php внутри нашего каталога Example. Так будет выглядеть примерное содержания для выбранной проверки:
<?php 
class Example_Sniffs_NamingConventions_HungarianNotationSniff
    
extends PHP_CodeSniffer_Standards_AbstractVariableSniff
{
    
    private 
$_variablePrefixes = array(
        
'b',    //bool
        
'a',    //array
        
'i',    //int
        
's',    //string
        
'f',    //float
        
'o',    //object
        
'm',    //mixed
    
);
    
    private 
$_predefinedVariables = array(
        
'this',
        
'GLOBALS',
        
'_SERVER',
        
'_GET',
        
'_POST',
        
'...'    //something else
    
);

    
/**
     * Processes class member variables.
     *
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param int                  $stackPtr  The position of token.
     *
     * @return void
     */
    
protected function processMemberVar(
        
PHP_CodeSniffer_File $phpcsFile
        
$stackPtr
    
) {
        
$tokens $phpcsFile->getTokens();

        
$memberProps $phpcsFile->getMemberProperties($stackPtr);
        if (empty(
$memberProps) === true) {
            return;
        }

        
$memberName     ltrim($tokens[$stackPtr]['content'], '$');
        
$scope          $memberProps['scope'];

        if (!
$this->_isVariablePredefined($memberName) && 
            !
$this->_isVariablePrefixed($memberName)
        ) {
            
$error 
                
'%s member var "%s" must be prefixed with a type code';
            
$data  = array(
                
ucfirst($scope),
                
$memberName,
            );
            
$phpcsFile->addError(
                
$error,
                
$stackPtr,
                
'MemberVarWithoutPrefix',
                
$data
            
);
        }
    }


    
/**
     * Processes normal variables.
     *
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param int                  $stackPtr  The position of token.
     *
     * @return void
     */
    
protected function processVariable(
        
PHP_CodeSniffer_File $phpcsFile,
        
$stackPtr
    
) {
        
$tokens $phpcsFile->getTokens();
        
        
$variableName     ltrim($tokens[$stackPtr]['content'], '$');
        
        if (!
$this->_isVariablePredefined($variableName) && 
            !
$this->_isVariablePrefixed($variableName)
        ) {
            
$error 
                
'Variable "%s" must be prefixed with a type code';
            
$data  = array(
                
$variableName,
            );
            
$phpcsFile->addError(
                
$error,
                
$stackPtr,
                
'VariableWithoutPrefix',
                
$data
            
);
        }
    }

    
/**
     * Processes variables in double quoted strings.
     */
    
protected function processVariableInString($phpcsFile$stackPtr)
    {
        
//TODO process variables in string
        
return;
    }
    
    
/**
     * Check if variable is predefined
     * 
     * @param  string $variableName Variable name
     * @return bool
     */
    
private function _isVariablePredefined($variableName)
    {
        return
            
in_array($variableName$this->_predefinedVariables);
    }
    
    
/**
     * Check if variable is correctly prefixed
     * 
     * @param  string $variableName Variable name
     * @return bool
     */
    
private function _isVariablePrefixed($variableName)
    {
        return 
            
preg_match(
                
'/^_?[' implode($this->_variablePrefixes). '][A-Z]/',
                
$variableName
            
);
    }
}
?>
Теперь попробуем что-нибудь проверить. Для этого у меня есть тестовый файл example.class.php с таким содержимом (обратите внимание, что тестовый файл не содержит никаких нарушений стандартов кодирования от PEAR):
<?php
/**
 * Example file for PHP_CodeSniffer
 * 
 * PHP version 5 
 * 
 * @category Example_File
 * @package  Example_File
 * @author   Eugene Khamukhin <khamukhin@gmail.com>
 * @license  Some free licence
 * @link     http://blog.yetanother.ru/
 */

/**
 * Example class for PHP_CodeSniffer
 * 
 * @category Example_Class
 * @package  Example_Class
 * @author   Eugene Khamukhin <khamukhin@gmail.com>
 * @license  Some free licence
 * @link     http://blog.yetanother.ru/
 */
class Example
{
    private 
$_cargo;
    
    
/**
     * Set cargo for space ship
     * 
     * @param ExampleCargo $cargo Cargo to deliver
     * 
     * @return null
     */
    
public function setCargo($cargo)
    {
        
$this->_cargo $cargo;
    }
    
    
/**
     * Deliver cargo via Planet Express
     * 
     * @return ExampleCargo
     */
    
public function diliver()
    {
        
//do some work
    
}
}
?>
Запускаем проверку на дурные запахи и изучаем результат:
phpcs --standard=Example example.class.php 

FILE: /home/khamukhin/phpcs/example.class.php
------------------------------------------------------------------------
FOUND 3 ERROR(S) AFFECTING 3 LINE(S)
------------------------------------------------------------------------
 25 | ERROR | Private member variable "_cargo" must be prefixed
 34 | ERROR | Variable "cargo" must be prefixed with a type code
 36 | ERROR | Variable "cargo" must be prefixed with a type code
------------------------------------------------------------------------

Time: 1 second, Memory: 3.50Mb
Вторая программа, о которой я бы хотел рассказать - это PHP Depend. Эта утилита вычисляет много различных метрик (о которых можно будет рассказать в отдельной статье), а также строит графики зависимостей пакета.

Использование PHP Depend более актуально в контексте сервера непрерывной интеграции. Например, вот так будет выглядеть запуск PHP Depend в скрипте сборки для Phing:
<!-- ============================================ -->
<!-- Target: run.phpdepend                        -->
<!-- ============================================ -->
<target name="run.phpdepend">
    <phpdepend>
        <fileset dir="${build.dir}/application/classes">
            <include name="**/*.php" />
        </fileset>
        <logger type="jdepend-xml"
            outfile="${build.dir}/build/jdepend.xml" />
        <logger type="jdepend-chart"
            outfile="${build.dir}/build/pdepend/dependencies.svg" />
        <logger type="overview-pyramid"
            outfile="${build.dir}/build/pdepend/overview-pyramid.svg" />
        <analyzer type="coderank-mode" value="method" />
    </phpdepend>
</target>
Последний инструмент, который будет рассмотрен в данной статье, это PHPMD - Mess Detector. Данная утилита позволяет проверить исходный код на потенциальные проблемы, не очень оптимальные участки кода, неиспользуемые параметры, методы и т.д. Вот как может выглядеть запуск phpmd:
phpmd classes text codesize,unusedcode,naming

classes/legacy.class.php:3 This class has too many methods, 
                                consider refactoring it.

classes/legacy.class.php:3 The class Legacy has an overall 
                                complexity of 68 which is very high. 
                                The configured complexity threshold is 50.
...
Для этой утилиты также можно настраивать свои правила, а как это сделать можно найти в документации. Для code review данный инструмент также, как и PHP_CodeSniffer является просто незаменимым.

На этом серия постов про полезные инструменты не завершается, продолжение следует!

28.10.2011

Незаменимые инструменты: phploc и phpcpd

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

Несколько статей в этом блоге будут посвящены таким тулзам, будут описаны основные возможности утилит. В этом посте я бы хотел рассказать о двух инструментах: Обе утилиты предлагает использовать известный и уважаемый разработчик Sebastian Bergmann, что уже является неплохой рекомендацией. Phploc и Phpcpd особенно актуальны для использования на сервере непрерывной интеграции, но и не будут лишними, находясь под рукой разработчика.

Начнем с Phploc. Эта утилита позволяет померить PHP проект, т.е. это такая своеобразная линейка, lines of code. Опций у phploc совсем мало: помимо указания каталога или файла и других не очень важных параметров, можно установить путь до лог-файла, в который phploc запишет результат измерений в XML или CSV.

Запускаем phploc:
phploc --log-csv reports/phploc.csv application/classes 

phploc 1.6.1 by Sebastian Bergmann.

Lines of Code (LOC):                               2992
  Cyclomatic Complexity / Lines of Code:           0.10
Comment Lines of Code (CLOC):                       587
Non-Comment Lines of Code (NCLOC):                 2405

Namespaces:                                           0
Interfaces:                                           0
Classes:                                              1
  Abstract:                                           0 (0.00%)
  Concrete:                                           1 (100.00%)
  Average Class Length (NCLOC):                    2660
Methods:                                             62
  Scope:
    Non-Static:                                      62 (100.00%)
    Static:                                           0 (0.00%)
  Visibility:
    Public:                                          27 (43.55%)
    Non-Public:                                      35 (56.45%)
  Average Method Length (NCLOC):                     42
  Cyclomatic Complexity / Number of Methods:       4.94

Anonymous Functions:                                  0
Functions:                                            0

Constants:                                            5
  Global constants:                                   0
  Class constants:                                    5
Два показателя (Cyclomatic Complexity / Lines of Code и Cyclomatic Complexity / Number of Methods) используют такую характеристику кода, как Цикломатическая сложность. Этот показатель характеризует число ветвей в коде и вычисляется путем подсчета операторов цикла, условного перехода и переключений. Цикломатическая сложность была разработана еще в 1976 году, а ее автор Томас Дж. Мак-Кейб предлагал заставлять разработчиков разбивать код на более простые участки, если сложность модуля превышает значение 10.

Вторая утилита, о которой будет рассказано в данном посте - это Phpcpd, детектор “копипаста”. Пожалуй, никто не будет спорить, что копипаст дурно пахнет и почти наверняка приведет к ошибкам в приложении, которые проявятся рано или поздно. Утилита phploc ищет такие дурные, повторяющиеся места в коде.
phpcpd --min-tokens 50 --verbose lib

phpcpd 1.3.2 by Sebastian Bergmann.

Processing files
49 / 49 [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] 100.00%

Found 3 exact clones with 66 duplicated lines in 4 files:

  - foo.class.php:241-252
    foo.class.php:285-296

  - copy.class.php:12-22
    paste.class.php:20-30

  - boo.class.php:833-848
    foo.class.php:391-406

0.25% duplicated lines out of 26439 total lines of code.

Time: 3 seconds, Memory: 24.25Mb
Параметры min-lines и min-tokens позволяют повышать или понижать планку того, что считать Copy/Paste. Опцией log-pmd результаты можно сохранить в формате PMD-CPD XML.

На этом пока все, продолжение следует. Делитесь в комментариях своими тулзами, которые помогают вам!

26.10.2011

Переходим на непрерывную интеграцию с Jenkins

В предыдущей статье мы подключили проект к серверу постоянной интеграции phpUnderControl. На этом мы не остановимся и теперь попробуем другой сервер непрерывной интеграции — Jenkins (ранее назывался Hudson).

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

Зачем использовать именно Jenkins? Его стоит использовать, потому что это превосходный проект, у которого есть огромное количество дополнений, его легко устанавливать и настраивать. Кроме того, для Jenkins существует мини-проект php-jenkins-template от Sebastian Bergmann (создатель PHPUnit и не только), который позволяет достаточно легко подключать PHP проекты. Да что долго говорить, лучше один раз попробовать!

Приступаем к установке. Установить Jenkins достаточно легко, например для Ubuntu нужно выполнить такие команды:
wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | \
sudo apt-key add -

sudo sh -c 'echo deb http://pkg.jenkins-ci.org/debian binary/ > \
/etc/apt/sources.list.d/jenkins.list'

sudo aptitude update
sudo aptitude install jenkins
Теперь можно приступить к установке php-jenkins-template. Подробное руководство по установке представлено на странице проекта, здесь же я отмечу только наиболее важные моменты.

Установить необходимые плагины (для PHP нужно установить checkstyle, cloverphp, dry, htmlpublisher, jdepend, plot, pmd, violations, xunit) можно как через терминал, так и через веб-интерфейс. Кстати, вероятно для многих главным различием между CruiseControl и Jenkins будет то, что для конфигурации первого необходимо работать преимущественно с командной строкой, а для Jenkins — в графическом интерфейсе. Прежде чем, начать установку плагинов, нужно обновить в Jenkins список доступных дополнений. Обновить этот список можно на странице: http://hostname:8080/pluginManager/advanced.

Следующим важным шагов будет установка самого шаблона для PHP проекта. Это можно сделать такими командами:
cd $JENKINS_HOME/jobs
git clone git://github.com/sebastianbergmann/php-jenkins-template.git php-template
chown -R jenkins:nogroup php-template/
Далее необходимо создать свою конфигурацию на основе заготовки для PHP проектов. Здесь самое главное проследить за корректностью путей до XML-логов, которые создаются при сборке проекта.

Для сборки проекта в этом примере мы будем использовать Phing (соответствующий плагин для Jenkins нужно также установить). За основу build-скприта проекта мы будем использовать файл, который уже рассматривался в статье про phpUnderControl и Phing. Но здесь мы добавим еще несколько задач: code.browser, run.phploc, run.phpdepend.
<?xml version="1.0" encoding="UTF-8"?>
<project name="Example" default="default">
    <property name="build.dir" value="." />

    <!-- ============================================ -->
    <!-- (DEFAULT) Target: default                    -->
    <!-- ============================================ -->
    <target name="default"
        depends="lint.legacy, run.tests.legacy, run.coverage, run.mess.detector, run.copypast.detector, run.phpcodesniffer, api.docs, code.browser, run.phploc, run.phpdepend">
        <echo msg="All done." />
    </target>

    <!-- ============================================ -->
    <!-- Target: api.docs                             -->
    <!-- ============================================ -->
    <target name="api.docs">
        <docblox title="API Documentation" destdir="${build.dir}/public/apidocs/"
            quiet="true">
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
        </docblox>
    </target>

    <!-- ============================================ -->
    <!-- Target: code.browser                         -->
    <!-- ============================================ -->
    <target name="code.browser">
        <exec
            command="phpcb --source ${build.dir}/classes --output ${build.dir}/public/code-browser"
            dir="${build.dir}" />
    </target>

    <!-- ============================================ -->
    <!-- Target: lint.legacy                          -->
    <!-- ============================================ -->
    <target name="lint.legacy">
        <phplint>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
        </phplint>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.tests.legacy                     -->
    <!-- ============================================ -->
    <target name="run.tests.legacy">
        <coverage-setup database="${build.dir}/build/coverage.db">
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
        </coverage-setup>
        <phpunit haltonfailure="true" haltonerror="true"
            codecoverage="true">
            <formatter type="xml" usefile="true"
                todir="${build.dir}/build/phpunit" />
            <formatter type="clover" usefile="true"
                todir="${build.dir}/build/phpunit" />
            <formatter type="plain" usefile="false" />
            <batchtest>
                <fileset
                    dir="${build.dir}/tests">
                    <include name="*Test.php" />
                </fileset>
            </batchtest>
        </phpunit>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.coverage                         -->
    <!-- ============================================ -->
    <target name="run.coverage" depends="run.tests.legacy">
        <coverage-report outfile="${build.dir}/build/coverage.xml">
            <report todir="${build.dir}/build/coverage"
                styledir="/usr/share/php/data/phing/etc" />
        </coverage-report>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.mess.detector                    -->
    <!-- ============================================ -->
    <target name="run.mess.detector">
        <phpmd>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="xml"
                outfile="${build.dir}/build/phpmd.xml" />
            <formatter usefile="false" type="text" />
        </phpmd>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.copypast.detector                -->
    <!-- ============================================ -->
    <target name="run.copypast.detector">
        <phpcpd>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="pmd"
                outfile="${build.dir}/build/phpcpd.xml" />
            <formatter usefile="false" type="default" />
        </phpcpd>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.phpcodesniffer                   -->
    <!-- ============================================ -->
    <target name="run.phpcodesniffer">
        <phpcodesniffer standard="Zend" showSniffs="true"
            showWarnings="true">
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <formatter type="checkstyle"
                outfile="${build.dir}/build/phpcodesniffer.xml" />
            <formatter type="summary" usefile="false" />
        </phpcodesniffer>
    </target>

    <!-- ============================================ -->
    <!-- Target: run.phploc                           -->
    <!-- ============================================ -->
    <target name="run.phploc">
        <exec
            command="phploc --log-csv ${build.dir}/build/phploc.csv ${build.dir}/classes"
            dir="${build.dir}" />
    </target>

    <!-- ============================================ -->
    <!-- Target: run.phpdepend                        -->
    <!-- ============================================ -->
    <target name="run.phpdepend">
        <phpdepend>
            <fileset dir="${build.dir}/classes">
                <include name="**/*.php" />
            </fileset>
            <logger type="jdepend-xml"
                outfile="${build.dir}/build/jdepend.xml" />
            <analyzer type="coderank-mode" value="method" />
        </phpdepend>
    </target>
</project>
Можно запустить сборку проекта вручную и, если все хорошо, смело включать проект в Jankins. За сборкой проекта в этом сервере непрерывной интеграции можно очень удобно следить:


В заключении я бы хотел поделиться своими субъективными наблюдениями:
  • в Jankins довольно удобно настраивать проект, а некорректная конфигурация одного проекта не сломает другие проекты в отличии от CruiseControl;
  • поражает количество плагинов, доступных для установки (например, с помощью некоторых плагинов можно отправлять сообщения о неудачных билдах в Jabber или Twitter);
  • интерфейс более объемный (больше разнообразных станиц для каждой сборки), чем в phpUnderControl;
  • если вы используете phpUnderControl, то как минимум посмотреть на Jenkins вы просто обязаны :)
Удачных экспериментов с непрерывной интеграцией и благодарю за внимание!