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

  1. Базовая структура
  2. Провайдеры аргументов
  3. Утверждения и матчеры
  4. Миры (фикстуры)
  5. Контексты
  6. Анонимные контейнеры
  7. Образцы
  8. Запуск
  9. Результат выполнения
  10. Включение/отключение узлов
  11. Обработка ошибок
  12. Отчеты
  13. Команды конструирования
  14. Плагины
  15. События
  16. Обратная связь

Spectrum — это PHP фреймворк, предназначенный для так называемых specification тестов (аналог RSpec и т.п.) и предоставляющий довольно богатые возможности по настройке и расширению.

Скачать исходные коды можно с github.com/mkharitonov/spectrum/

* Все примеры и сама статья пока проверялись только в браузерах Chrome и Firefox.

<?php require_once 'spectrum/spectrum/init.php'; describe('Космический корабль', function(){ it('Должен бороздить просторы вселенной', function(){ $spaceship = new Spaceship(); be($spaceship->getLocation())->eq('space'); }); it('Не должен прохлаждаться', function(){ $spaceship = new Spaceship(); be($spaceship->getTask())->not->eq('foo'); }); }); \net\mkharitonov\spectrum\RootDescribe::run();

Результат:

Базовая структура

Для написание спецификаций Spectrum предоставляет несколько различных команд конструирования. Базовая древовидная структура создается с помощью команд конструирования it(), describe() и context() (про контексты см. в нижеследующем разделе), притом ни глубина ни кол-во команд никак не ограничено.

Пример вложенной структуры:

describe('Космический корабль', function(){ it('Должен бороздить просторы вселенной', function(){ be(true)->true(); }); describe('Боевое оснащение', function(){ it('Должены быть установлены фотонные пушки', function(){ be(true)->true(); }); it('Должены быть установлены лезеры', function(){ be(true)->true(); }); }); describe('Медицинский отсек', function(){ it('Должен быть оборудован душевой кабиной', function(){ be(true)->true(); }); it('Должен содержать 5 койко-мест', function(){ be(true)->true(); }); describe('Операционная', function(){ it('Должна быть размером 9 кв. м.', function(){ be(true)->true(); }); it('Должна быть оборудована системой стерилизации', function(){ be(true)->true(); }); }); }); });

Результат:

Провайдеры аргументов

Бывает, что сами данные сами по себе являются лучшим описанием требуемого поведения. Для таких случает существуют поставщики данных (data providers).

Пример провайдеров аргументов:

describe('Форма ввода телефона', function(){ it('Должена принимать различные форматы тефонных номеров', array( '+7 (495) 123-456-7', '(495) 123-456-7', '123-456-7', ), function($world, $tel){ be($tel)->eq('+7 (495) 123-456-7'); }); // Если требуется передать несколько аргументов, то элемент провайдера аргументов должен сам быть массивом it('Должена принимать различные форматы тефонных номеров', array( 'foo', array('bar', 'bar2'), 'baz' ), function($world, $arg1, $arg2 = null){ be($arg1)->eq('bar'); be($arg2)->eq('bar2'); }); });

Результат:

Утверждения и матчеры

Утверждения создаются с помодью команды конструирования be(), которая возвращает объект класса core/Assert, обращаясь к которому можно вызывать различные матчеры (вызовы методов перехватываются и перенаправляются к требуему матчеру).

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

Пример различных вызовов матчеров:

it('Должен', function(){ be(true)->true(); be(true)->not->false(); // Так же можно записывать несколько матчеров в одной строке (обратите внимание // что "not" действует только на первый из последующих матчеров) be(true)->not->false()->true(); });

Результат:

Вы так же можете добавлять собственные матчеры с помощью команды конструирования addMatcher($name, $callback). Callback функции будет передано актуальное значение первым параметром и все агрументы, переданные матчеру при вызове, последующими параметрами.

Пример callback функций матчеров:

describe('', function(){ addMatcher('foo', function($actual){ return ($actual == 'foo'); }); it('Должен', function(){ be('foo')->foo(); be('bar')->not->foo(); }); // Матчер с дополнительными параметрами addMatcher('something', function($actual, $expected, $elseArg){ // $actual - foo // $expected - bar // $elseArg - baz return false; }); it('Должен еще', function(){ be('foo')->something('bar', 'baz'); }); });

Результат:

С помощью команды addMatcher($name, $callback) можно добавить матчер в любой it/describe/context и дочерние команды конструирования будут вызывать матчеры из своих родителей/предков (в случае объявления матчеров в контекстах, поиск матчера будет осуществляться в стеке выполняющихся контекстов, см. подробности в главе про контексты ниже).

Пример матчеров во вложенных структурах:

describe('Пример добавления', function(){ addMatcher('foo', function(){ return true; }); it('Должен', function(){ be(true)->foo(); // Из родителя }); describe('Второй', function(){ it('Должен', function(){ be(true)->foo(); // Из предка (т.е. оттуда же, что и предыдущий) }); }); }); describe('Пример переопределения', function(){ addMatcher('foo', function(){ return true; }); it('Должен', function(){ be(true)->foo(); }); describe('Со своей версией foo', function(){ addMatcher('foo', function(){ return false; }); it('Должен', function(){ be(true)->foo(); }); }); });

Результат:

Spectrum содержит некоторое кол-во стандартных матчеров, которые уже добавлены в RootDescribe.

Пример переопределения стандартных матчеров:

describe('', function(){ it('Должен', function(){ be(null)->null(); be(true)->true(); be(1)->not->true(); be(false)->false(); be(0)->not->false(); be('foo')->eq('foo'); be(new Spaceship())->not->ident(new Spaceship()); be(5)->lt(10); // Less than be(10)->ltOrEq(10); // Less than or equal be(10)->gt(5); // Greater than be(10)->gtOrEq(10); // Greater than or equal be(function(){ throw new Exception(); })->throwException(); be(function(){ throw new ErrorException(); })->throwException('\ErrorException'); be(function(){ throw new ErrorException('Foo is not bar', 123); })->throwException('\ErrorException', 'foo', 123); be(function(){ throw new Exception('Foo is not bar'); })->throwException(null, 'foo'); }); it('Должен', function(){ be(function(){ throw new Exception(); })->throwException('\ErrorException'); }); it('Должен', function(){ be(function(){ throw new ErrorException(); })->throwException('\ErrorException', 'foo'); }); // Использование мира в throwException() it('Должен', function($world){ be(function() use($world){ $world->foo = 'bar'; })->not->throwException(); }); });

Результат:

Поскольку все стандартные матчеры содержатся в RootDescribe, их можно с легкостью переопределить.

Пример переопределения стандартных матчеров:

addMatcher('true', function($actual){ return ($actual !== true); }); describe('', function(){ it('Должен', function(){ be(true)->true(); }); });

Результат:

Миры (фикстуры)

Т.к. природа BDD тестов предполагает детальное описание поведения, во многих случаях код смежных it() будет дублироваться. Что бы избежать этого, в Spectrum'е существуют миры (worlds), которые создаются при помощи команд конструирования beforeEach($callback) и afterEach($callback) и в которые следует выносить общие для всех it() части.

Пару слов о терминологии. У каждого it() есть свой мир (world), к которому могут применяться творцы мира (world creators): beforEach() — строитель (builder) и afterEach() — разрушитель (destroyer).

Пример создания мира:

describe('Космический корабль', function(){ beforeEach(function($world){ // World является простым объектом без каких-либо свойств, поэтому можно «наживую» создавать // в нем любые свойства, а так же просматривать все его свойства в цикле foreach $world->spaceship = new Spaceship(); $world->spaceship->startSystems(); }); afterEach(function($world){ $world->spaceship->stopSystems(); }); it('Должен бороздить просторы вселенной', function($world){ be($world->spaceship->getLocation())->eq('space'); }); it('Не должен прохлаждаться', function($world){ be($world->spaceship->getTask())->not->eq('foo'); }); });

Результат:

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

Пример создания мира:

describe('Космический корабль', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->startSystems(); }); afterEach(function($world){ $world->spaceship->stopSystems(); }); it('Должен бороздить просторы вселенной', function($world){ be($world->spaceship->getLocation())->eq('space'); }); describe('Межпространственный полет', function(){ beforeEach(function($world){ $world->spaceship->startEngine(); }); afterEach(function($world){ $world->spaceship->stopEngine(); }); it('Должен облетать звёзды', function($world){ new Star(25, 50, 100); $world->spaceship->setDestination(30, 50, 100); be($world->spaceship->isHasCollision())->false(); }); }); });

Результат:

Порядок применения творцов к мирам дочерних узлов — во внутрь. Т.к. Сначала строители родителя, потом строители ребенка, затем разрушители ребенка, потом разрушители родителя.

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

Контексты

Суть контекстов можно продемонстрировать на примере, когда требуется одни и те же it() выполнить в разных мирах (тобишь при разных внешних условиях).

Пример без использования контекстов:

describe('Космический корабль', function(){ describe('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); describe('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); // Копипастим it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); describe('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('rest'); // Отличается }); // Снова копипастим it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); });

Результат:

С помощью контекстов можно избавить себя от скушных copy/paste.

Пример с использованием контекстов:

describe('Космический корабль', function(){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('rest'); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); });

Результат:

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

  1. Если у describe() есть context() дети (не потомки), то выполняются только эти контексты;
  2. Если у context() есть context() дети (не потомки), то так же выполняются только эти контексты;
  3. Если у context() нет context() детей, то данный контекст запускает выполнение все describe/it до ближайшего describe предка (т.е. сначала запускает describe/it детей ближайшего describe предка, затем describe/it детей промежуточных контекстов, а затем своих детей);

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

describe('Космический корабль', function(){ // Используем анонимный контекст context(function(){ beforeEach(function($world){ // Выносим общий код создания экземпляра класса $world->spaceship = new Spaceship(); }); context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship->setTask('rest'); }); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); });

Результат:

Устранять дублирования не только в двумерных describe():

describe('Космический корабль', function(){ context(function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); }); context('В галактиках', function(){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship->setTask('rest'); }); }); }); context('На планетах', function(){ context('На планете Земля', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('На планете Марс', function(){ beforeEach(function($world){ $world->spaceship->setTask('rest'); }); }); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); });

Результат:

Добавлять при необходимости в какой-либо контекст дополнительные спецификации (как it(), так и describe(), который в свою очередь может содержать любое кол-во потомков, включая контексты):

describe('Космический корабль', function(){ context(function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); }); context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('rest'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship->setTask('rest'); }); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); });

Результат:

Создавать многоуровневые контексты:

describe('Космический корабль', function(){ // В данном случае анонимный контекст можно опустить, разместив творцов на // одном уровне с контекстами, к которым он должен применяться beforeEach(function($world){ $world->spaceship = new Spaceship(); }); context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); describe('Миссия', function(){ context('В космосе', function(){ beforeEach(function($world){ $world->spaceship->setTask('study'); }); }); context('На планете', function(){ beforeEach(function($world){ $world->spaceship->setTask('rest'); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); });

Результат:

Вы так же можете объявлять свои матчеры в каждом контексте:

describe('Космический корабль', function(){ context(function(){ context('В галактике Альфа Центавра', function(){ addMatcher('foo', function($actual){ return ($actual == 'foo'); }); }); context('В галактике Хоага', function(){ addMatcher('foo', function($actual){ return ($actual == 'foo'); }); }); context('В галактике Мейола', function(){ addMatcher('foo', function($actual){ return ($actual == 'bar'); }); }); }); it('Должен быть как Foo', function(){ be('foo')->foo(); }); });

Результат:

Анонимные контейнеры

Команды конструирования describe() и context() могут принимать callback функцию в качестве единственого аргумента (либо можно задать пустую строку в качестве имени). В таком случае речь идет об анонимном контейнере, имена которых будут исключаться из отчетов, но которые по прежнему можно использовать для физической организации.

Пример анонимного describe:

describe('Космический корабль', function(){ it('Должен (из именного контекнера)', function(){ be(true)->true(); }); describe(function(){ // Тут, например, можно добавить творцов миров (или матчеры), которые // будут применяться только к детям и потомкам данного describe it('Должен (из анониимного контейнера)', function(){ be(true)->true(); }); it('Должен (из анониимного контейнера)', function(){ be(true)->true(); }); }); });

Результат:

Образцы

Так же для устранения дублирований можно использовать образцы.

Пример анонимного describe:

addPattern('Автомобиль', function($doorsCount){ it('Кол-во дверей должно быть ' . $doorsCount, function($w) use($doorsCount){ be(4)->eq($doorsCount); }); }); itLikePattern('Автомобиль', 4); itLikePattern('Автомобиль', 3);

Результат:

Запуск

Запускать можно не только корневой describe, но и любой другой узел, ссылку на которым можно получить с помощью плагина selector, начиная поиск от RootDescribe, либо присвоив результат работы it/describe/context переменной.

Все корневые узлы добавляются в класс RootDescribe, который сам по себе является анонимным describe (точнее, это статический класс, у которого есть метод getOnceInstance(), возвращающий экземпляр класса core\SpecContainerDescribe), поэтому вызов метода RootDescribe->getOnceInstance()->run() (RootDescribe::run() просто удобный алиас) ведет себя аналогично другим узлам.

$spec = describe('Космический корабль', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function(){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function(){ be(true)->true(); }); }); $spec->run(); // В данном случае аналогично RootDescribe::run()

Результат:

Можно запустить только дочерние узлы.

$spec = describe('Космический корабль', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function(){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function(){ be(true)->true(); }); }); $spec->selector->getChildByIndex(1)->run(); // Нумерация начинается с нуля

Результат:

Можно так же передать ссылку на переменную в замыкание, куда и сохранить результат.

describe('Космический корабль', function() use(&$spec){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); $spec = it('Должен собирать неизвестные ископаемые', function(){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function(){ be(true)->true(); }); }); $spec->run();

Результат:

Можно запускать целые describe.

describe('Космический корабль', function() use(&$spec){ it('Должен', function(){ be(true)->true(); }); $spec = describe('Миссия', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function(){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function(){ be(true)->true(); }); }); }); $spec->run();

Результат:

Если есть контексты, узел будет запущен во всех из них.

describe('Космический корабль', function() use(&$spec){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('rest'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); $spec = it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); $spec->run();

Результат:

Запуск узла в многоуровневом контексте так же возможен.

context('Некий контекст', function(){}); context('Еще один контекст', function(){}); describe('Космический корабль', function() use(&$spec){ context('В галактике Альфа Центавра', function(){}); context('В галактике Хоага', function(){}); context('В галактике Мейола', function(){}); it('Должен изучать живые организмы', function($world){ be(true)->true(); }); $spec = it('Должен собирать неизвестные ископаемые', function($world){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function($world){ be(true)->true(); }); }); $spec->run();

Результат:

Можно так же запустить и все узлы какого-либо контекста.

describe('Космический корабль', function() use(&$spec){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('rest'); }); }); context('В галактике Хоага', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); $spec = context('В галактике Мейола', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); $world->spaceship->setTask('study'); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен собирать неизвестные ископаемые', function($world){ be($world->spaceship->getTask())->eq('study'); }); it('Должен защищать слабых и обездоленных', function($world){ be($world->spaceship->getTask())->eq('study'); }); }); $spec->run();

Результат:

Результат выполнения

Помимо отображения результата в отчете, метод run() возвращает результат запуска.

  1. false — тест (и или один из дочерних тестов) не пройден
  2. true — тест (и тесты всех дочерних узлов) пройден (нет непройденных и пустых тестов)
  3. null — тест (либо тест одного из дочерних узлов) не содержит ни одного положительного утверждения или ошибки
$spec = describe('Космический корабль', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function(){ be(false)->true(); }); it('Должен защищать слабых и обездоленных', function(){ }); describe('Миссия', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); it('Должен служить людям', function(){}); }); }); $spec->run();

Результат:

Включение/отключение узлов

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

$spec = describe('Космический корабль', function(){ it('Должен изучать живые организмы', function($world){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function($world){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function($world){ be(true)->true(); }); it('Должен служить людям', function($world){ be(true)->true(); }); }); $spec->selector->getChildByIndex(0)->disable(); $spec->selector->getChildByIndex(2)->disable(); $spec->run();

Результат:

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

$spec = describe('Космический корабль', function(){ describe(function(){ it('Должен изучать живые организмы', function($world){ be(true)->true(); }); it('Должен собирать неизвестные ископаемые', function($world){ be(true)->true(); }); }); it('Должен защищать слабых и обездоленных', function($world){ be(true)->true(); }); it('Должен служить людям', function($world){ be(true)->true(); }); }); $spec->selector->getChildByIndex(0)->disable(); $spec->run();

Результат:

Можно так же отключить и контексты.

describe('Космический корабль', function() use(&$spec, &$context){ context('В галактике Альфа Центавра', function(){}); context('В галактике Хоага', function(){}); $context = context('В галактике Мейола', function(){}); it('Должен изучать живые организмы', function($world){ be(true)->true(); }); $spec = it('Должен собирать неизвестные ископаемые', function($world){ be(true)->true(); }); it('Должен защищать слабых и обездоленных', function($world){ be(true)->true(); }); }); $context->disable(); $spec->run();

Результат:

Основная идеология, которой придерживается Spectrum при отключении узлов — отключение не должно изменять поведение тестов (только результаты). В следующем примере, отключение всех контекстов не заставит Spectrum выполнять it узлы describe (хотя именно такое поведение распространяется на describe без дочерних контекстов). В данном случае это просто привело бы к ошибке, т.к. it ожидает наличие переменной $world->spaceship

$spec = describe('Космический корабль', function(){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); }); }); it('Должен изучать живые организмы', function($world){ be($world->spaceship)->eq(new Spaceship()); }); }); $spec->selector->getChildByIndex(0)->disable(); $spec->run();

Результат:

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

$spec = describe('Космический корабль', function(){ context('В галактике Альфа Центавра', function(){ beforeEach(function($world){ $world->spaceship = new Spaceship(); }); }); it('Должен изучать живые организмы', function($world){ if (isset($world->spaceship)) be($world->spaceship)->eq(new Spaceship()); else be(true)->false(); }); }); $spec->selector->getChildByIndex(0)->removeFromParent(); $spec->run();

Результат:

Обработка ошибок

Spectrum позволяет управлять обработкой ошибок на уровне каждого из узлов с помощью следующих методов плагина errorHandling:

  1. $spec->errorHandling->setCatchExceptions(true|false) — перехватывать исключения, выбрасываемые тестами и добавлять их в результирующий буффер
  2. $spec->errorHandling->setCatchPhpErrors(true|false|errorLevel) — перехватывать ошибки (те, которые может отловить set_error_handler()), генерируемые тестами и добавлять их в результирующий буффер
  3. $spec->errorHandling->setBreakOnFirstPhpError(false) — прерывать выполнение теста при первой php ошибке или нет
  4. $spec->errorHandling->setBreakOnFirstMatcherFail(false) — прерывать выполнение теста при первом провальном утверждении (матчере) или нет

Результирующий буффер — это экземпляр класса core\RunResultsBuffer, который создается при каждом выполнении it и в который заносятся результаты всех утверждения и ошибок. В этот буфер можно добавить свои результаты, например, из плагинов и просмотреть его содержимое в любой момент.

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

Все эти параметры можно задавать для конкретных it/describe/context.

Отчеты

Отчет об ошибках включается/выключается в плагине liveReport. По умолчанию, включены. Отчет об ошибках можно включать/отключать для конкретных it/describe/context

$spec = describe('Космический корабль', function(){ it('Должен изучать живые организмы', function(){ be('foo')->eq('foo'); be('foo')->eq('bar'); }); }); $spec->run();

Результат:

Команды конструирования

Spectrum спроектирован тиким образом, что команды конструирования полностью отделены от объектной структуры. Например, следующие два примера дадут одинаковый результат.

use \net\mkharitonov\spectrum\RootDescribe; describe('Космический корабль', function(){ it('Должен изучать живые организмы', function(){ be(true)->true(); }); }); RootDescribe::run();

Результат:

То же, но без использования команд конструирования:

use \net\mkharitonov\spectrum\RootDescribe; use \net\mkharitonov\spectrum\core\SpecItemIt; use \net\mkharitonov\spectrum\core\asserts\Assert; use \net\mkharitonov\spectrum\core\SpecContainerDescribe; $it = new SpecItemIt('Должен изучать живые организмы'); $it->setTestCallback(function(){ $assert = new Assert(true); $assert->true(); }); $describe = new SpecContainerDescribe('Космический корабль'); $describe->addSpec($it); RootDescribe::getOnceInstance()->addSpec($describe); RootDescribe::run();

Результат:

Команды конструирования делятся на два этапа:
  1. Обьявление (declaring)
  2. Выполнение (running)

На этапе обьявления вызываются callback функции таких команд, как describe и context. Callback ф-я it вызывается на этапе выполнения (т.е. после вызова метода run()).

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

Управление командами конструирования возложено на класс constructionCommands\Manager, который позволяет регистрировать новые или переопределять текущие команды.

use \net\mkharitonov\spectrum\constructionCommands\Manager; Manager::registerCommand('crash', function(){ Manager::getCurrentItem()->getRunResultsBuffer()->addResult(false, 'Космический корабль разбился'); }); it('Полет космического корабля', function(){ Manager::crash(); }); // А можно создать глобальный алиас Manager::createGlobalAliasOnce('crash'); it('Полет космического корабля', function(){ crash(); });

Результат:

Команды конструирования можно вызывать как угодно и откуда угодно.

Можно генерировать их динамически:

for ($i = 0; $i < 5; $i++) { it("Должен $i", function() use($i){ be($i)->lt(2); }); }

Результат:

А можно и подключать файлы, содержащие команды конструирования:

// specs.php // it('Должен изучать живые организмы', function(){ be(true)->true(); }); describe('Космический корабль', function(){ include('specs.php'); });

Результат:

Плагины

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

  1. Spec
  2. SpecContainer
  3. SpecContainerDescribe
  4. SpecContainerContext
  5. SpecContainerArgumentsProvider
  6. SpecItem
  7. SpecItemIt

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

В нижеследующем примере, у каждого из трех узлов (экземпляров классов SpecContainerDescribe, SpecItemIt и, опять же, SpecItemIt) существует свой экземпляр плагина foo.

\net\mkharitonov\spectrum\core\plugins\Manager::registerPlugin('foo'); $describe = describe('Космический корабль', function() use(&$it1, &$it2){ $it1 = it('Должен летать', function(){}); $it2 = it('Не должен плавать', function(){}); }); $describe->foo->add('foo'); $it1->foo->add('bar'); $it2->foo->add('baz'); print '<pre>'; print_r($describe->foo->getAll()); print_r($it1->foo->getAll()); print_r($it2->foo->getAll());

Результат:

Методу Manager::registerPlugin() принимает 3 параметра:

  1. Имя для доступа к плагину (foo в примере выше);
  2. Класс плагина, который должен реализовывать интерфейс core\plugin\PluginInterface (по умолчанию — это базовый плагин core\plugins\basePlugins\stack\Indexed);
  3. Момент создание экземпляра (активации) плагина:
    1. whenConstructOnce — экземпляр плагина будет создан во время создания соответствующего экземпляра узла;
    2. whenCallOnce (используется по умолчанию) — экземпляр плагина будет создан только при первом обращении к нему, а при повторном обращении будет возвращаться ранее созданный экземпляр;
    3. whenCallAlways — при каждом обращении будет создаваться (и возвращаться) новый экземпляр плагина.

Создаваемый класс плагина не обязательно реализовавыть с нуля. Можно унаследовать его от класса core\plugin\Plugin (который, в частности, содержит полезный метод callCascadeThroughRunningContexts(), позволяющий получить значение вызова функции через стек запущенных контекстов).

use \net\mkharitonov\spectrum\core\plugins\Manager as PluginsManager; use \net\mkharitonov\spectrum\constructionCommands\Manager as ConstructionCommandsManager; class MyPlugin extends \net\mkharitonov\spectrum\core\plugins\Plugin { private $foo = null; // Значение по умолчанию должно задаваться не здесь, а при вызове callCascadeThroughRunningContexts() public function setFoo($foo) { $this->foo = $foo; } public function getFoo() { return $this->foo; } public function getFooCascade() { return $this->callCascadeThroughRunningContexts( 'getFoo', // Вызываемый метод array(), // С аргументами 'foo', // Значение по умолчанию (возвращаемое, если требуемое значение не найдено вплоть до самого верхнего уровня) null // Если getFoo возвратит идентичное значение, то поиск будет продолжен выше ); } } PluginsManager::registerPlugin('foo', 'MyPlugin'); $spec = describe('Космический корабль', function() use (&$context1, &$context2, &$context3){ $context1 = context('В галактике Альфа Центавра', function(){}); $context2 = context('В галактике Хоага', function(){}); $context3 = context('В галактике Мейола', function(){}); it('Должен', function(){ print '<strong>' . ConstructionCommandsManager::getCurrentItem()->foo->getFooCascade() . '</strong>'; }); }); $context1->foo->setFoo('bar'); $context2->foo->setFoo('baz'); // А для $context3 будет возвращего значение по умолчанию $spec->run();

Результат:

Так же можно унаследовать плагин от одного из базовых плагинов, самым востребованным из которых, возможно, является плагин Stack (и конечные классы Indexed и Named), позволяющий работать со стеком значений, в частности, получить значения через стек запущенных контекстов (именно от данного плагина и унаследованы плагины Matchers и WorldCreators).

События

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

На текущий момент доступны следующие интерфейсы плагинов (расположенные в core\plugin\events).

  1. OnRunInterface — методы вызываются при запуске в узлах любого типа
    1. onRunBefore()
    2. onRunAfter($result)
  2. OnRunContainerInterface — методы вызываются при запуске в узлах контейнерного типа (SpecContainer*)
    1. onRunContainerBefore()
    2. onRunContainerAfter($result)
  3. OnRunItemInterface — методы вызываются при запуске в узлах типа SpecItem*
    1. onRunItemBefore()
    2. onRunItemAfter($result)
  4. OnTestCallbackCallInterface — методы вызываются непосредственно при выполнении тестовой функции (и могут получать сведения о мире, в отличии от OnRun событий)
    1. onTestCallbackCallBefore(core\World $world)
    2. onTestCallbackCallAfter(core\World $world)

Порядок вызова событий следующий:

onRunBefore onRunContainerBefore или onRunItemBefore Применение строителей к миру onTestCallbackCallBefore Вызов testCallback onTestCallbackCallAfter Применение разрушителей к миру onRunContainerAfter или onRunItemAfter onRunAfter

Обратная связь

Связаться с автором можно по e-mail адресу.