Skip to content

basisjs/yatra

Repository files navigation

NPM version

Yatra

Yatra (Yet Another Test Runner) – "еще один" исполнитель модульных тестов (unit-test runner).

В основном Yatra расчитан на тестирование фреймворка basis.js и проектов, разрабатываемых с его применением. Но это не является обязательным требованием и Yatra может использоваться в проектах и без basis.js.

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

На данный момент, реализован не весь планируемый функционал. В будущем возможности будут расширяться (см. TODO).

Как использовать

Yatra поставляется в трех видах:

  • reporter – как приложение;
  • lib – как библиотека, для встраивания в другие интерфейсы;
  • runner – непосредственно иполнитель тестов, не включает в себя интерфейс; предназначен для использования с другими тестирующими фреймворками и системами;

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

Установка

Для установки сборки потребуется npm:

> npm install yatra --save-dev

Использование в исходном виде

Можно использовать Yatra и в исходном виде. Для этого необходимо клонировать репозитарий и установить зависимости:

> git clone https://github.com/basisjs/yatra.git yatra
> cd yatra
> npm install

Основной файл интерфейса – src/reporter.html.

Сборка:

> npm run build

Результат сборки окажется в папке build.

Использование в проекте

Когда сборка Yatra добавлена в проект, нужно настроить его использование. В данном репозитории, можно найти примеры настройки для проектов на basis.js example/basis_setup и для проектов без него - example/non_basis_setup.

// TODO

Пример использования можно посмотреть в репозитории basis.js, выглядит это так:

Как выглядит

TODO

  • придумать название :)
  • полностью тестировать сам себя
  • доделать стиль
  • выводить в summary не только ошибки, но информацию об общем ходе выполнения тестов
  • beforeEach/afterEach (+done/async)
  • поддержка Promise
  • возможность задавать порог, что тест медленно отрабатывает
  • возможность быстрой отладки
  • возможность делать тесты производительности
  • возможность использовать различные assert библиотеки
  • добавить различные интерфейсы: bdd, tdd, exports, qunit
  • автоматизивароть сборку и зарегистрировать в bower
  • прогон тестов используя node.js (зарегистрировать в npm)
  • поддержка code coverage
  • интеграция с travis ci

Описание тестов

Тесты описываются в виде вложенной структуры из объектов и массивов. Важными полями являются name и test. Первое задает подпись к тесту (название), а второе - либо сам тест, либо набор вложенных тестов.

var myTestSuite = {
  name: 'Test suite',
  test: [
    {
      name: 'Test #1',
      test: function(){
        // test code
      }
    },
    {
      name: 'Test #2',
      test: function(){
        // test code
      }
    }
  ]
};

Если значением поля test является функция, то это непосредственно сам тест. Если массив, то набор тестов (suite).

Когда используется basis.js, тесты и их наборы можно выносить в отдельные файлы и подключать используя функцию require. Это облегчает навигацию по тестам.

Обычно описывается корневой пакет тестов (index.js), который может выглядеть так:

// test suite
module.exports = {
  name: 'Example test suite',
  html: __dirname + 'env.html', // базовый файл окружения
  test: [
    require('./spec/suite1.js'),
    require('./spec/suite2.js'),
    require('./spec/suite3.js'),
    require('./spec/suite4.js')
  ]
};

Этот файл подключает другие файлы, являясь своего рода входной точкой. Наборы тестов описываются в отдельных файлах и, обычно, располагаются в папке spec.

Свойство html задает файл, который будет использоваться для задания окружения выполняемым тестам. Такой файл загружается в iframe, и код тестов выполняется в рамках этого фрейма. Значение свойства html наследуется вложенными тестами. При необходимости его можно переопределить для определенного теста или поддерева тестов.

Типовой набор тестов (suite):

module.exports = {
  name: 'Test suite',
  init: function(){
    // ...
  },
  test: [
    // тесты
  ]
};

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

Переменные объявленные в init будут доступны всем тестам. Упрощенный код как это работает:

// выбираем код тела функции init и выполняем его в текущем окружении
eval(getFunctionBody(testSuite.init));

// функция которая исполняет код тестов
function runTest(code){
  // выполняется код тела функции, но сама функция не вызывается
  eval(getFunctionBody(code));
}

// тесты выполняются один за другим
runTest(test1);
runTest(test2);
// ...

Если у теста (или набора) есть свойство html или init, то для него и его вложенных тестов создается собственное окружение (отдельный iframe).

Утверждения

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

Несколько неверных утверждений

Для проверки утверждения используется функция assert, которая доступна как локальная переменная. Функция может принимать один или два два аргумента:

assert(actual);                  // проверяется, что значение правдиво, то есть
                                 // не равно '' (пустая строка), null, undefined, false, 0 и NaN
assert(expected, actual);        // проверяется, что actual равно expected
assert.deep(expected, actual);   // глубокая проверка, что actual равно expected

Здесь actual это проверяемое значение, а expected - то значение которое ожидается.

При сравнении expected и actual делается проверка соотвествия типов и значений. Если тип совпадает, и этот тип массив или объект, то делается нечеткое сравнение значений, то есть проверяется их похожесть: все ключи и значения тождественно равны (===). Для применения того же правила к вложенным значением используется функция assert.deep(expected, actual).

assert([1, 2, 3], [1, 2, 3]); // ok
assert([{ foo: 1 }, { bar: 2 }], [{ foo: 1 }, { bar: 2 }]); // ошибка, разные объекты
assert.deep([{ foo: 1 }, { bar: 2 }], [{ foo: 1 }, { bar: 2 }]); // ok

Если для проверки значения достаточно использовать операторы === или ==, то такое выражение рекомендуется записывать единственным аргументом assert. Yatra поймет, что левая часть это проверяемое значение, а правая - ответ.

assert(actual === expected);

Сравнение

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

Исключения

Если в ходе выполнения теста возникает исключение, то выполнение теста прекращается. В этом случае будет показано на какой строке возникла проблема:

Исключение

Если исключение является ожидаемым и требуется проверить оно возникает, то такой код нужно обернуть в функцию и передать методу assert.exception() или его синониму assert.throws().

module.exports = {
  name: 'Exception example',
  test: function(done){
    var foo = 123;

    assert.exception(function(){
      foo.exception();  // будет выброшено исключение, так как у чисел нет метод exception
    });
  }
};

Если исключение будет выброшено, то утвержение будет считаться верным. Иначе будет считаться ошибкой. При возникновении исключений внутри функции обернутой assert.exception() выполнение остального кода теста не прерывается. assert.exception() является укороченной записью для следующего кода:

// тестирование ожидаемого исключения без assert.exception()
module.exports = {
  name: 'Exception example',
  test: function(done){
    var foo = 123;
    var throwsException = false;

    try {
      foo.exception();  // будет выброшено исключение, так как у чисел нет метод exception
    } catch(e) {
      throwsException = true;
    }

    assert(throwsException === true);
  }
};

Асинхронные тесты

Для написания асинхронных тестов, есть несколько возможностей.

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

module.exports = {
  name: 'Async test',
  test: function(done){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      if (xhr.readyState == 4)
      {
        assert(xhr.status === 200);
        done();
      }
    };
    xhr.open('GET', 'file.txt', true);
    xhr.send();
  }
};

Для выполнения асинхронных проверок не рекомендуется использовать setTimeout или setInterval. Для это нужно использовать метод assert.async, которому передается функция для выполнения в следующем фрейме. Внутри таких функций так же может быть вызван метод assert.async. Тест считается выполняющимся пока не выполнена хотя бы одна функция, заданная через assert.async.

module.exports = {
  name: 'Async test',
  test: function(){
    var foo = 1;

    // для примера, значение меняется через 50ms
    setTimeout(function(){
      foo = 2;
    }, 50);

    assert.async(function test(){
      if (foo !== 2)
        assert.async(test);

      assert(foo === 2);
    });
  }
};

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

Если в ходе выполнения кода теста, возникает исключение, то выполенение теста прекращается, а еще не выполненные функции, выставленные через assert.async, вызваны не будут.

Каждому тесту, если он выполняется асинхронно, отводится 250ms. Это значение можно изменить на уровне теста, задав свойство timeout с необходимым значением. Если по истечении этого времени все еще будут не выполнены часть функций, то тест будет считаться заваленым.

module.exports = {
  timeout: 1000,  // увеличенный таймаут (по умолчанию 250ms)
  name: 'Async test with custom timeout',
  test: function(done){
    callSomeAsyncFunction(function(){
      done();
    });
  }
};

Если функция теста возвращает Promise (значение с методом then), то тест будет считаться завершенным, когда разрешится Promise. Как будет разрешен Promiseresolve или reject – не влияет на результат теста.

module.exports = {
  name: 'Await promise is resolved',
  test: function(){
    return new Promise(function(resolve, reject){
      setTimeout(function(){
        resolve();
      }, 10);
    });
  }
};

Визитер

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

module.exports = {
  name: 'Visited points',
  test: function(){
    visit('foo');
    visit({ bar: true });

    assert.visited(['foo', { bar: true }]);
  }
};

Метод visit.list() позволяет получить текущий копию списка точек, а visit.reset() очищает этот список.

module.exports = {
  name: 'visit.list() and visit.reset()',
  test: function(){
    visit({ foo: 'test' });
    assert.deep([{ foo: 'test' }], visit.list()); // эквивалентно assert.visited({ foo: 'test' });

    visit.reset();
    assert.deep([], visit.list());
  }
};

Для того, чтобы отметить некоторую точку, что она не должна посещаться используется метод visit.wrong().

module.exports = {
  name: 'visit.wrong()',
  test: function(){
    visit.wrong(); // эффект тот же, что и assert(false), но в данном случае
                   // будет выведено более говорящее сообщение
  }
};

Метод visit.wrap() позволяет обернуть метод, чтобы записывать его вызовы. В список точек будет сохраняться первый аргумент:

module.exports = {
  name: 'visit.wrap()',
  test: function(){
    var obj = {
      foo: function(obj){
        return obj;
      }
    };

    visit.wrap(obj, 'foo'); // перед выполнением метода в список точек сохранится первый аргумент

    obj.foo(1, 2);
    obj.foo({ bar: 1 });

    assert.visited([1, { bar: 1 }]);
  }
};

Результат тестов

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

Если тест не содержит утверждений, то он считается пропущенным (pending) вне зависимости от того какой код в нем выполняется. Можно намеренно пропустить выполнение теста или набора тестов, задав свойство pending равным true.

module.exports = {
  pending: true,  // пропустить тест
  name: 'Pending test example',
  test: function(done){
    console.log('Этот код не будет выполнен');
  }
};

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

Примеры различных ситуаций и варианты отображения можно посмотреть на примере example/showcase.html.

Пример различных ситуаций