Совершенствуем Simpletest


На главную

ВНИМАНИЕ
Данная статья была написана несколько лет назад и нуждается в актуализации. В ближайшем будущем статья будет отредактирована. Если вы стоите перед выбором фреймворка для тестирования, рекомендую обратить внимание на PHPUnit, который развивается более активно, чем simpletest.

Скачать надстройку (wtest.zip)

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

На самом деле, все это уже реализовано. Вам остается только установить надстройку над SimpleTest и наследовать свои тестовые классы от предложенного WUTest. ;)

Содержание

initCase и cleanCase

Начнем с того, что-первых, методы setUp и tearDown выполняются для каждого отдельного теста, что не подходит для глобальной инициализации кейса, когда мы хотим выполнить инициализацию, общую для всех тестов юнита. Эту задачу можно было решить путем объявления метода глобальной инициализации первым методом класса и метода сборки мусора последним. Однако UnitTestCase абсолютно все равно с каким результатом завершается тот или иной тестовый метод. Это означает, что если по каким то причинам наш метод инициализации провалится, то выполнение тестов будет продолжено как ни в чем не бывало, хотя никакого смысла в этом уже нет. В итоге такого развития событий на выходе мы получаем кучу провалов и исключений, что абсолютно не способствует разбору полетов. Мы бы предпочли, что бы наши тесты не выполнялись вообще, чем разбирать кашу из сообщений о вполне закономерных ошибках, которые вывалятся в случае ошибки инициализации.

Прерывание тестов при провале

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

function testBasics(){
    if (!$this->assertTrue(class_exists("ObjectMother"))) return;
    $this->assertNotNull($this->om = new ObjectMother);
    if (!$this->assertTrue(method_exists($this->om,"pushObject"))) return;
    if (!$this->assertTrue(method_exists($this->om,"cleanUp"))) return;
}
Согласитесь, не совсем удобно на каждый чих писать один и то же if/return? И это при том, что последующие тесты так же будут выполнены, независимо от результата, что в данном случае абсолютно бессмысленно, так как сама тестируемая сущность недоступна (класс ObjectMother не определен). Гораздо приятнее выглядит вот этот код
function testBasics(){
    $this->assertTrue( class_exists("ObjectMother") );
    $this->assertNotNull( $this->om = new ObjectMother );
    $this->assertTrue( method_exists($this->om,"pushObject") );
    $this->assertTrue( method_exists($this->om,"cleanUp") );
}
Как видим, здесь нет никаких лишних операторов. Глядя на этот код аж дышать легче, по этому следующим требованием к тестеру у нас будет возможность прерывания последовательности в случае провала одного из тестов.

NTR ассерты

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

В реализации WTest к NTR ассертам относятся любые стандартные (реализованные в SimpleTest), к именам которых добавляется префикс из нижнего подчеркивания (_assertFalse, _fail). Пример использования NTR ассертов

public function testPrepareController(){
        $ctrl = new PersistentContainer();
        $obj = new PersistentContainerTest_Persistent();
        
        $ctrl->attributes2Requisites($obj);
        // Убедимся, были ли созданы реквизиты
        $this->_assertTrue($ctrl->isRequisiteExists("id"));
        $ctrl->setRequisite("r_ref",new RequisiteText("r_ref"));

        // убедимся что объект в нужном нам состоянии
        $this->_assertFalse($obj->isNew());
        $this->_assertFalse($obj->isSelected());
    
        // далее тесты непосредственно по процессу подготовки

Тестирование исключение

Вместо прежнего

    try {
            $ctrl->getBearingRequisiteId();
            $this->fail("Fail at line ".__LINE__);
        }catch( Exception $e ){ $this->pass(); }
теперь пишем
        try {
            $cc->addController("",$c1);
        }catch( Exception $e ){} $this->assertException($e);

        try {
            $cc->addController("c1",$c1,"prefix_");
        }catch( Exception $e ){} $this->assertNotException($e);
Внутри сетки могут быть другие ассерты. Единственное требование - вызывать один из ассертов assertException или assertNotException обязательно. Это нужно для того что бы снаружи не беспокоиться о $e = null перед каждой сеткой и для того, что бы обработать ассерты, проваленные внутри сетки. Вторым аргументом указывается класс исключения, который по дефолту равен "Exception".

Раннер

Runner позволяет гибко выбирать цели. Например имеются следующие каталоги и файлы внутри каталога unit_test
Core.test.php
MyClass.test.php
orm/Atom.test.php
orm/Persistent.test.php
mvc/Controller.test.php
mvc/io/InputDriver.test.php
mvc/io/OutputDriver.test.php
Раннер выглядит так
// run.php
include_once("wtest/wtestarter.class.php");
include("unit_test.conf.php");

$tests = new WTestarter;
$tests->getTests()->run($tests->getReporter());
$tests->showExecutionTime();

Из командной строки:
запустить все тесты
> php run.php

запустить все тесты mvc
> php run.php mvc

запустить тест атома
> php run.php orm/Atom.test

запустить тесты mvc и orm, пропустив те что в корне
> php run.php mvc orm

запустить тесты orm и MyClass
> php run.php orm MyClass.test

запустить тесты драйверов
> php run.php mvc/io

Состав пакета

В пакет входят следующие классы:

  • WException
  • WInvoker
  • WGroupTest
  • WTestarter
  • WUTest
Здесь мы имеем один из тех немногих случаев, когда использование исключений действительно оправдано, так здесь не подразумевается вообще какая-либо обработка ошибочных ситуаций. Исключения используется для форсирования стека при провале assert-ов. Перехват исключений выполняется в классе WInvoker, наследующий от SimpleInvoker, который обволакивает исполнение тестового метода сеткой try/catch.

Выброс исключения инициирует класс WUTest, который является наследником UnitTestCase. WUTest перегружает метод createInvoker в котором и выполняет конструкцию экземпляра WInvoker вместо SimpleInvoker. Метод assertTrue так же перегружается, его новая реализация подразумевает выброс исключения при провале вызова базовой реализации метода. Кроме того, в класс WUTest добавлены методы initCase и cleanCase, которые вызываются соответственно перед и после выполнения тестов (вызов базовой реализации метода run). Как следует из названия, эти методы предназначены для общей инициализации юнит-теста. Здесь следует заметить, что в случае провала initCase тесты выполняться не будут.

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

И последний класс WTestarter - это один из вариантов организации тестового пространства. Данный класс реализует три механизма определения списка тестов:

  • непосредственное определение списка тестов через аргументы командной строки;
  • определение списка тестов в файле очереди;
  • определение списка тестов на основе результатов сканирования директории
С использованием WTestarter код запуска тестов значительно сокращается
// unit_tests.php
include_once(wtest/wtestarter.class.php");
$tests = new WTestarter();
$tests->getTests()->run($tests->getReporter());
По умолчанию WTestarter использует подкаталог unit_test текущего каталога в качестве каталога тестов и unit_test/_tests_queue в качестве имени файла очереди исполнения тестов. Если возникает необходимость определить другой каталог тестов или другое имя файла очереди, можно модифицировать публичные атрибуты класса WTestarter->tests_path и WTestarter->tests_queue соответственно. Так же предусмотрен прием этих значений в конструктор.

Теперь для того, что бы запустить какие-либо отдельные тесты, например находящиеся в файле some_unit_test.php, необходимо выполнить команду

% php unit_tests.php some_unit_test
Если такой файл существует в каталоге тестов, то будут выполнены все тесты, объявленные в этом файле а так же тесты, которые подключены в этом файле посредством include или аналогичными способами.

Без указания аргументов, WTestarter сначала просматривает файл очереди. Имена файлов тестов (без расширения .php) будут добавлены в группу тестов в том порядке, в котором они определены в файле очереди. В случае если файла очереди не существует, WTestarter построит список тестов на основании содержимого каталога тестов. Следует учитывать что в последнем случае последовательность выполнения тестов не определена. Так же следует помнить что тесты, имена которых соответствуют регулярке /^\./ не будут включены в группу для исполнения.

Наверх


Правила использования | На главную Whirlwind © 2002 - 2012

ИЯндекс цитирования