Spectrum — PHP фреймворк для BDD тестирования (alpha версия)

mkharitonov

Новичок
Если Вы об этом? http://www.phpunit.de/manual/3.6/en/fixtures.html, то зачем выносить фикстуру из метода, если для каждого она своя? Когда всё рядом, то и нагляднее получается.
Я имел ввиду, что для нескольких тестовых методов из множества может потребоваться создать свою фикстуру.

Ну и потом, PHPUnit это ООП-шный фрэймворк со всеми вытекающими, т.е. его легко расширять, переопределять и т.д.
Как я уже говорил, все это расширение ограничивается одномерной структурой методов в классе.

Прежде чем браться за улучшение какой то определенной технологии, было бы неплохо хорошенько узнать плюсы и минусы предшественников.
Пожалуйста, не говорите абстрактными фразами. Я упустил какие-то плюсы или не заметил минусов? Ведь писал я этот фреймворк не просто ради того, что бы создать аналог RSpec, а ради решения проблем, с которыми я сталкивался в PHPUnit. Например:
1) Для создания собственных отрицательных утверждений надо создавать дополнительный метод (нету аналога записи be('actual')->not->null()).
2) Что бы создать фикстуру для группы методов надо либо создавать доп. класс, либо выносить код фикстур в отдельные методы и вызывать их в каждом тестовом методе.
3) Неудобно организовывать тесты в виде одномерного списка. Все таки описание работы трестируемого метода (особенно того, который вызывает внутри себя приватные методы), как правило, требует создания, как минимум, нескольких тестовых методов. И, согласитесь, эти методы удобнее разместить в древовидном виде (с отступами в соответствии с уровнем вложенности и т.п.). Притом удобство это не только визуальное, но оно же еще и обеспечивает возможность приминения общих фикстур к таким группам (облажающих по определению высокой связностью), создания утверждений в пределах группы и т.п.
4) Не возможно описывать тесы на русском языке. Если Вы и все участники проектов, над которыми Вы работаете (естественно, проектов на PHP) свободно владеют английским языком, то я за Вас очень рад. В противном случае это ограничение будет либо подталкивать к именованию тестовых методов в виде testИмяТестируемогоМетодаИНеБолее и запахиванию в один такой тестовый метод кучи проверок, либо именованию тестовых методов трудночитаемым транслитом, либо затратам на время, проведенное программистами за словарем и учебником по грамматике (надеюсь понятно, что именование обычных методов двумя-тремя словами на английском языке и составлением целых предложений, описывающих ожидаемое поведение метода — это разные вещи).
5) Если потребуется выполнить одни и те же тесты в разном окружении, то придется потрудиться (в spectrum'е можно воспользоваться контекстами).

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

К тому же есть более серьезные проблемы, которые не решаются ни в рамках TDD ни рамках BDD.
Буду признателен за ссылки на статьи/темы, где можно подробнее почитать о данных проблемах.
 

AmdY

Пью пиво
Команда форума
mkharitonov
1. у assert есть аналоги с Not - assertNotNull, assertAttributeNotContains....
2. собственно это и есть фича, что методы тестируются независимо
3.дерево сложнее списка, воспринимать jQuery стайл очень сложно. у вас же код не будет таким же древовидным, они будет последовательным, зачем его тестить по другому.
4. у assert-во есть параметр который указывает какое сообщение выводить в случае фэйла, пишите его на русском. какая разница как метод именуется.кстати, у вас разработчики которые не владеют английским языком реально именуют и методы с переменными на русском?
5. проект работает в одном окружении. даже тестовое окружение - это зло.

я вообще не люблю тесты, за исключением разумных процентов ~10% покрытия проекта. но у вас кроме излишнего тестирования, ещё придуманы кучи проблем для самих тестов, которые вы с блеском решили.
у вас ВСЕГО один пример из реальной жизни - с номером телефона и он уже меня пугает
PHP:
describe('Форма ввода телефона', function(){
	it('Должена принимать различные форматы тефонных номеров',
	array('123-456-7', '+7 (495) 123-456-7', '(495) 123-456-7'),
	function($world, $tel){
		if (preg_match('/\+/', $tel))
			actual(false)->beTrue();
		else
			actual(true)->beTrue();
	});

	// Если требуется передать несколько аргументов, то элемент поставщика данных должен сам быть массивом
	it('Должена принимать различные форматы тефонных номеров', array(
		'foo',
		array('bar', 'bar2'),
		'baz'
	), function($world, $arg1, $arg2 = null){
		actual(true)->beTrue(); // do something
	});

	// Так же поставщик данных может быть функцией, которая должна будет вернуть массив
	it('Должена принимать различные форматы тефонных номеров',
	function(){ return array('foo', array('bar', 'bar2'), 'baz'); },
	function($world, $arg1, $arg2 = null){
		actual(true)->beTrue(); // do something
	});
});
 

whirlwind

TDD infected, paranoid
Основная проблема тестов что TDD, что BDD - это условность покрытия. Тесты не покрывают все возможные варианты взаимодействия, тем самым, частные случаи могут быть рассмотрены только тогда, когда они известны до разработки тестов. Из всех известных мне фреймворков, только google test наиболее близко подошел к решению этой проблемы: http://code.google.com/p/googletest/source/browse/trunk/samples/sample8_unittest.cc (см Combine). Естественно, что тут более подходящего варианта чем параметризованные тесты придумать трудно. Все остальные пункты, перечисленные Вами выше, кроме пятого, исключительно дело вкуса, о которых как мы знаем не спорят. Пятый пункт как раз прекрасно решается в рамках TDD, который заставляет делать такой дизайн кода, который будет выполняться в любом окружении. Достигается это за счет правильной декомпозиции интерфейсов. Если вы постараетесь решить пункты с 1 по 4 за счет правильной декомпозиции, вы получите хороший дизайн и хорошие тесты.
 

mkharitonov

Новичок
Ваш пример не слишком показательным получится, его можно (и следует) значительно улучшить и средствами PHPUnit. Плюс у Вас в одном тесте вперемешку, по-видимому, тесты представления (те, что на селениуме) и контроллера.
Тест для метода checkArray (тот, что содержится у Вас в testIsEqualToField) из Вашего примера будет выглядеть примерно так:

PHP:
describe('Класс Builder', function()
{
    describe('Метод checkArray', function()
    {
        beforeEach(function($world)
        {
            $source = '
                <form name="form">
                   <input type="text" name="value" id="in">
                   <input type="text" name="value2" id="in2">
                   <!--
                   
                    <form>{ var= "$form" }
                    
                   <form>value {
                    isEqualToField=value2: @#in@+error 
                   }
                   
                   <form>value2 {
                    required: @#in@+error
                   }
                   -->
                </form>';
            
            $builder = new \TrustedForms\CodeGenerator\Builder('phpQueryTemplate','PHPCode');
            $builder->buildFile($source);
            
            eval($builder->getResultValidator()); // here $form will be defined
            $world->form = $form;
        });

        it('Должен порождать ошибку при сравнении разных значений', function()
        {
            $form->checkArray(array(
                'value'     => 'one',
                'value2'    => 'two'
            ));
            
            actual($form->isError())->beTrue();
        });
        
        it('Не должен порождать ошибок при сравнении одинаковых значений', function()
        {
            $form->checkArray(array(
                'value'     => 'two',
                'value2'    => 'two'
            ));
            
            actual($form->isError())->beFalse();
        });
        
        // И т.д.
    });
    
    describe('Validators info', function(){
        // И т.п.
    });
});

 

tz-lom

Продвинутый новичок
Это не тесты предствления, это валидаторы на стороне клиента, они должны быть идентичны валидаторам на стороне сервера поэтому тестировать в месте их мне кажется более правильным решением
А как предлагаете улучшить в PHPUnit ?
и я правильно понял что на ту кучу тестовых данных надо будет написать такую же кучу почти одинаковых методов?
 

mkharitonov

Новичок
1. у assert есть аналоги с Not - assertNotNull, assertAttributeNotContains....
Я имел ввиду, что создавая свои assert методы, приходится во многих случаях писать излишюю Not версию метода.

2. собственно это и есть фича, что методы тестируются независимо
При чем тут независимое выполнение тестов? Я же вообще о другом писал.

3.дерево сложнее списка, воспринимать jQuery стайл очень сложно. у вас же код не будет таким же древовидным, они будет последовательным, зачем его тестить по другому.
Видимо мы опять о разном. Здесь я говорил о случае, когда тестовые методы именуются так:
PHP:
testSomeMethod_SomeParamEnabled_ShouldBe()
testSomeMethod_SomeParamDisabled_ShouldBe()
В этом случае получается, то древовидную структуру приходится "вытягивать" в плоский, одномерный вид. Такие псевдо списки мне кажутся для восприятия сложнее (это как в XML отступы у вложенных элементов убрать и у каждого элемента записать имена всех его предков).

4. у assert-во есть параметр который указывает какое сообщение выводить в случае фэйла, пишите его на русском. какая разница как метод именуется.
Я придерживаюсь мнения, что в названии метода должно быть описано тестируемое поведение. Если перенести его в message у assert'а, то как тогда методы именовать? test1,2,3? Это не позволит не только лог тестирования получить, но и приведет к тому, что тесты для одного метода будут разбросаны по всему классу.

кстати, у вас разработчики которые не владеют английским языком реально именуют и методы с переменными на русском?
Я говорил о названиях тестовых методов (в которых описывается суть теста), а не о названиях переменных и обычных методов.

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

я вообще не люблю тесты, за исключением разумных процентов ~10% покрытия проекта.
Если Вы считаете достаточным 10% покрытие кода, то с проблемами по организации, которые данный фреймворк решить и пытается, Вы, скорей всего, и не столкнетесь (если только, Вы и не ограничеваете кол-во тестов как раз из-за проблем с организацией).


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

 
  • Like
Реакции: AmdY

mkharitonov

Новичок
Из всех известных мне фреймворков, только google test наиболее близко подошел к решению этой проблемы: http://code.google.com/p/googletest/source/browse/trunk/samples/sample8_unittest.cc (см Combine).
Спасибо, на досуге изучу.

Основная проблема тестов что TDD, что BDD - это условность покрытия.
Вообще, контексты в spectrum'е я как раз с этой целью и делал: что бы покрыть больше вариантов при меньших усилиях. И если описывать поведение программы в древовидном виде, то можно для дерева объявить несколько корней (контекстов; которые, в свою очередь, могут быть вложенными) и выполнить все тесты-потомки в каждом их этих корней (и для тестов-потомков все это будет проходить прозрачно).
 

mkharitonov

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

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

Для начала Вам стоит задуматься о вынесении части программы, отвечающей за генерацию HTML кода (той, что Вы селениумом тестируете) в отдельный класс, который можно будет протестировать отдельно. Если по каким-то причинам Вам этого сделать не удастся, то можно разместить селениум тесты в отдельном классе (что бы запускать их отдельно, без всяких SKIP_SELENIUM_TESTS), можете унаследовать этот класс и другой от общего, где разместить общие методы (для создания форм ($source = <<<HEREDOC) и т.п.).

PHP:
if((strpos(self::$selenium->getAttribute("value@class"),'error')===false)!=$result)
{
    $this->fail("Test `{$this->description}` don't ".($result?'pass':'fail')." on `{$data}` in JS module");
}
// ...
$this->assertFalse(strpos(self::$selenium->getAttribute("value@class"),'error')===false);
В PHPUnit есть методы assertContains() и assertNotContains(), которые могут принимать message.

Дата провайдер validatorsInfo я бы упростил до простого массива (т.е. убрал бы genTestConfig(), т.к. он довольно сильно усложняет понимание, устраняя при это лишь незначительную часть дублирования). Т.е. методы testValidator(), genTestConfig() и validatorsInfo() я бы переписал примерно так (надеюсь, что я ничего не упустил):
PHP:
    /**
     * @dataProvider validatorDataProvider
     */
    public function testValidator($validator,$data,$result)
    {
        $this->prepareTest($validator);
        $this->performTest($data, $result);
    }

    public function validatorDataProvider()
    {
        return array(
            array('isNumeric', '2', true),
            array('isNumeric', '3.14', true),
            array('isNumeric', '3.14', true),
            array('isNumeric', '-3', true),
            array('isNumeric', '-2.18', true),
            array('isNumeric', '0.12', true),
            array('isNumeric', '-0.17', true),
            array('isNumeric', '+3.24', true),
            array('isNumeric', '1.6e5', true),
            array('isNumeric', '-1.1e-2', true),

            array('isNumeric', 'a', false),
            array('isNumeric', '-a', false),
            array('isNumeric', '0.1z', false),

            array('inRange = (1,5)', '2', true),
            // ...
        );
Ну и т.п.

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