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 функции будет передано актуальное значение первым параметром и все агрументы, переданные матчеру при вызове, последующими параметрами.
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'); });
});
Работают контексты согласно следующим правилам:
В частности, это позволяет использовать вложенные контексты для еще большего устранения дублирований в творцах:
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('Космический корабль', function(){
it('Должен (из именного контекнера)', function(){ be(true)->true(); });
describe(function(){
// Тут, например, можно добавить творцов миров (или матчеры), которые
// будут применяться только к детям и потомкам данного describe
it('Должен (из анониимного контейнера)', function(){ be(true)->true(); });
it('Должен (из анониимного контейнера)', function(){ be(true)->true(); });
});
});
Так же для устранения дублирований можно использовать образцы.
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() возвращает результат запуска.
$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:
Результирующий буффер — это экземпляр класса 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();
На этапе обьявления вызываются 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 являются следующие классы узлов:
Плагины представляют собой объекты соответствующих классов (зарегистрированных через мереджер плагинов), которые создаются для каждого экземпляра 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 параметра:
Создаваемый класс плагина не обязательно реализовавыть с нуля. Можно унаследовать его от класса 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).
Порядок вызова событий следующий:
onRunBefore
onRunContainerBefore или onRunItemBefore
Применение строителей к миру
onTestCallbackCallBefore
Вызов testCallback
onTestCallbackCallAfter
Применение разрушителей к миру
onRunContainerAfter или onRunItemAfter
onRunAfter