test.it — не опять, а снова

в 12:39, , рубрики: javascript, open source, tdd, тестирование, метки: , ,

Добрый день хабр.
После моей статьи о test.it прошла вечность неделя. И как я не планировал растянуть этот срок хотя бы на месяц, но пришло время для новой публикации.
Картинка для привлечения внимания:
test.it — не опять, а снова
С того времени библиотека (во многом благодаря хабравчанам) обросла новым функционалом.
А ввиду того, что синтаксис, приведённый в прошлой статье, в текущей версии работает не полностью, откладывать эту статью ещё на 3 недели у меня нету права.

Кто не любит много слов — Сайт на котором можно увидеть код в действии, GitHub, Wiki

Появились цепочные вызовы

test.it(some)
     .comment('comment to test')
     .callback(function(){alert('test has been passed')})
     .arguments(); // -> [some]
test.group('group', function(){ ... })
     .comment('comment to group')
     .result(); // -> true/false

и новый механизм вложенности

test.group('first group',function(){
  ...
  test.group('second group', function(){
    ...
  });
  ...
});
test.group('first group').group('second group',function(){ ... });

Новый способ отображения ошибок
error

И два дополнительных метода test.typeof() и test.trace().

А также 3 Wiki-страницы.

А теперь обо всём этом поподробнее.

И так. Разберём пример, приведённый в wiki:

Пока мы ещё не приступили к тестам, воспользуемся методом test.typeof().

console.log(
    test.typeof(1)
   ,test.typeof("text")
   ,test.typeof([1,2,3])
   ,test.typeof({a:1,b:2})
   ,test.typeof()
   ,test.typeof(document)
   ,test.typeof(document.getElementsByTagName("body"))
   ,test.typeof(window)
   ,test.typeof(/yes it is RegExp/) // and many many more ...
);

test.typeof() — определяет тип переданного ему значения.

Он умеет различать: Array, Boolean, Date, Error (EvalError, RangeError, ReferenceError, SyntaxError, TypeError, UriError), Function, NaN и Number, Object, RegExp, String, Window, HTML, NodeList. А ещё он пустую переменную определит как 'undefined' но в отличие от стандартного typeof не сможет получить в качестве аргумента не объявленную переменную. За то ответит 'undefined' на несуществующее свойство объявленного и непустого объекта. Но это уже специфика языка.

Если мы обновим страницу, то в консоли появится строка:
typeof

Теперь взглянем на метод test.trace().

(function firstFunction() {
    (function secondFunction() {
        (function lastFunction() {
            console.log(test.trace());
        })();
    })();
})();

test.trace() — возвращает список (собранный в строки разделённые "n") строк кода, которые были выполнены для вызова этого метода.

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

К выводу в консоль теперь добавится:
trace
Здесь и далее не значащие части вывода консоли на скриншотах будут опускаться, что бы не увеличивать бессмысленно размер изображений.

Давайте приступим к тестам.

Для начала создадим объект для тестов, переменную с целочисленным значением и пустую переменную.

var Family = {
    name: "Desiderio",
    pet: {
        type: "dog",
        name: "google"
    },
    members: [
        {
            name: "Titulus",
            age: 23
        },
        {
            name: "Dude",
            age: Infinity
        }
    ]
}

var myIQ = 100;

var Nothing;

Думаю вопросов касательно этого кода возникнуть не должно.

Поехали дальше.

Следующий шаг — первый тест.

Тест на не-ложность.

Тут всё как и раньше.

test.it("hello world");

test.done();

test.it( value ) — создаёт новый тест, проверяя value на не-ложность.
test.done() — завершает тесты и выводит результат в консоль.
Далее будет предполагаться, что test.done() идёт последней строкой нашего кода. В примерах я буду его опускать.

В консоли мы видим:
root
Где:

  • root — имя группы нулевого уровня.
  • pass — статус группы, означающий что все тесты/группы в ней пройдены успешно.
  • 1/0/0 — количество соответственно пройденных/проваленных/ошибочных тестов/групп.
  • (9 ms) — время в миллисекундах потраченное на тесты.

Если раскрыть эту группу, то можно увидеть список тестов/групп в ней.
root expanded
Прежде чем разбирать наш единственный тест, давайте раскроем и его:
test expanded
И так:

  • pass — статус теста означающий что он пройден
  • argument is not false — описание типа теста и его результата
  • [«hello world»] — массив аргументов переданных тесту

Попробуем воспользоваться механизмом цепочности.

Самый элементарный случай — добавление комментария:

test.it(2+2==5)
    .comment("i badly taught algebra at school");

.comment( text ) — добавляет комментарий к тесту/группе, в чьей цепочке он был вызван. При этом продолжая цепочку, но об этом чуть позже.

Этот код можно было бы записать и в виде:

test.it(2+2==5).comment("i badly taught algebra at school");

Но, для случаев с более длинными цепочками, запись в столбик удобнее и нагляднее.

Теперь root (раскрытый автоматически, потому что содержит непройденный тест) выглядит так:
2 tests
В счётчиках в первой строке можно заметить увеличение второго числа с 0 до 1, что означает увеличение колличества проваленных тестов/групп.

Обратим своё внимание на, раскрытый по умолчанию (потому что не был пройден), тест.
Он отличается от предыдущего только статусом fail — означающим что тест провален, и комментарием «i badly taught algebra at school», который мы добавили.

Очевидно что в test.it( value ) можно передавать не только строки, но и переменные, и выражения, и вызовы функций и т.п. Впринципе, что бы мы не передали, сначала будет выполненно, получен результат, а этот результат уже и пойдёт в тест. Таков уж JavaScript.

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

test.it(Infinity>Infinity-1)
    .comment("philosophically is not it?");

Можете подумать об этом выражении за рюмочкой кофе, мы здесь для другого собрались. Результат теста выглядит как и результат предыдущего, что очевидно.
infinity

Пока не мы не ушли в дебри цепочных вызовов, а мы обязательно уйдём, посмотрим на другой вариант теста test.it().

Тест на равенство

Давайте сравним, объявленную ранее переменную с другим значением.

test.it(myIQ,"genious")
    .comment("is I'm genious?");
test.it(myIQ,(1+10)*12 - 34 + 5*5*5 - 123)
    .comment("check my IQ to be a normal");

test.it( value1, value2 ) — проверяет равенство двух переданных ей значений.

В консоли эти 2 теста будут выглядеть следующим образом:
IQ
Ничего необычного, но стоит обратить внимание на описание первого (проваленного) теста. "arguments has different types" — в этом тексте содержится подсказка, поясняющая нам почему тест был провален — переданные аргументы разного типа.

А теперь попробуем более сложные цепи.
Давайте выполним какое-нибудь действие, в зависимости от результата теста.

if (test.it(Family)
        .comment("Is Family exist? Is it not empty?")
        .result()) {
    console.info("by if: ","Yep! Here it is!");
} else {
    console.warn("by if: ","ALARM! there are no Family");
}

.result() — завершает цепочку и возвращает результат теста.

В этом коде, мы проверяем Family на не-ложность, и в зависимости от результата выводим в консоль разные фразы.
Вывод консоли теперь выглядит так:
by if

Правда такую задачу предпочтительнее выполнять при помощи другого цепочного вызова:

test.it(Nothing)
    .comment("Is Nothing exist? Is it not empty?")
    .callback(
        function(){console.info("by callback: ","Yep! Here it is!");}
       ,function(){console.warn("by callback: ","ALARM! there are no Nothing");});

.callback( function(){ /* funcIfpass */}[, function(){ /* funcIffail */}[, function(){ /* funcIferror */}]]) — выполняет одну из трёх функций, в зависимости от результата прохождения теста или группы. При этом продолжая цепочку.
В результате в консоли мы видим:
by callback
Эта конструкция предпочтительней, потому что не завершает цепочку.

Группы

Теперь создадим первую группу.

test.group("Empty group",function(){});

test.group( name, function(){… } ) — создаёт новую группу или обращается к уже существующей, и заполняет её тестами, находящимися в функции, переданной вторым аргументом.

Но так как тестов переданно ен было, группа создаётся пустой, но со статусом pass — потому как в ней нету ниодного проваленного теста.
empty group
Перед тем как сделать скриншот, я расскрыл группу — это видно по повороту стрелки вниз, и двум серым пикселам, означающим её конец. Но так как группа пустая, она выглядит практически как закрытая.

Что ж. Приступим к более осмысленным действиям. Создадим группу с двумя тестами внутри и комментарием.

test.group('Family tests',function(){
    test.it(Family.name,"Zukerberg")
        .comment("Are we test Zukerberg's family?");
    test.it(Family.name,"Desiderio")
        .comment("Or Desiderio's?");
}).comment("unite!");

Вот видите — ничего сложного!
group with tests
Тут стоит лишь обратить внимание на добавленный нами комментарий unite! — в заголовке группы.

А теперь провернём финт ушами, и добавим несколько тестов в уже созданную группу. И не просто тестов, а тестов которые инициируют новые тесты в зависимости от своего результата.
Добавим следующий код:

test.group("Family tests",function(){

    test.it(Family.pet)
        .comment("Do we have a pet?")
        .callback(function(){
            // I add test in your test, so you can test while you testing
            test.it(Family.pet,{type:"dog", name:"google"})
                .comment("Is it right pet?");
        });

    test.it(Family.house)
        .comment("Do we have a House?")
        .callback(function(){
            // next test will not be executed
            test.it(Family.pet,{type:"Huge", color:"green"})
                .comment("Is it right House?"); 
        });
        
});

Учитывая прошлые 2 теста в этой группе и, описанные только что, ещё 4 теста, всего их будет = 5(sic!). Можете проверить на калькуляторе.
additional tests
Вон видите в заголовке? 3 пройденных, 2 проваленных — всего 5.

Новые тесты

Пора взглянуть на парочку необычных тестов. Для начала в группе «here comes strange tests» создадим следующие два теста:

test.them([Family.pet, Family.members])
    .comment("There must be memebers with pet, to call it a 'Family'");

test.types([Family.pet.name, Family.name],"string")
    .comment("Is names are string type");

test.them( values ) — аналог test.it( value ), только в качестве первого аргумента берёт массив, в котором уже проверяет элементы на не-ложность.
test.types( values [, type] ) — как и test.them( values ) проверяет элементы массива, переданного первым аргументом. Но проверяет их на совпадение типов, а если передан второй аргумент type — то берёт этот тип в качестве образца.
У этого теста есть упрощённый аналог, но о нём чуть-чуть позже.

Вот так они выглядят в консоли:
them and types
Я раскрыл тесты вместе с их массивами аргументов для наглядности, но что-то мне кажется, наглядность от этого только уменьшалась.

А вот вам и ещё одна цепочная магия:

var numberOfMembers = test.type(Family.members,"Array")
    .comment("Is it a several members, nor a single member?")
    .arguments()[0].length;
test.it(numberOfMembers>5)
    .comment("Is it big family?");

.arguments() — завершает цепочку вызовов и возвращает аргументы переданные в тест (не в группу!).
arguments
Поясню — первый тест, проверил значение Family.members на не ложность. Так как это массив из двух элементов — тест пройден.
arguments()[0] == Family.members. Следовательно в переменную numberOfMembers заносится количество элементов массива Family.members то бишь 2. Второй тест проваливается из-за того что 2 не больше 5.

Вложенность

Вы ведь ещё помните, что мы находимся в группе "here comes strange tests"?
Добавим сюда ещё одну группу, и сразу воспользуемся конструкцией for для того что бы создать несколько однотипных тестов.

test.group("Members age",function(){
    for (i=0;i<numberOfMembers;i++) {
        test.it(Family.members[i].age>25)
            .comment("Is "+Family.members[i].name+" older then 25?");
    }
});

members age
Теперь эта новая группа "Members age" располагается в старой "here comes strange tests".

Ошибки

Добавим в эту же группу "Members age" ещё один тест:

test.it()
    .comment("yep, here is error");

Такой код приведёт к ошибке, потому как test.it() ожидает получить от 1 до 2 аргументов.
error
В заголовке ошибки:

  • RangeError — тип ощибки
  • at least one argument expected — описание, помогающее понять причину её возникновения.

Затем идёт результат test.trace() что бы легче было найти её в коде. И сам объект ошибки, в данном случае RangeError — это если кому-то захочется покопаться в нём глубже.

Ссылки на группы

Вернёмся на уровень root.
На всякий случай напомню, что группа "here comes strange tests" сейчас выглядит так:
big group
В ней есть ещё одна группа "Members age". Вот в неё тест мы сейчас и добавим.

test.group("here comes strange tests").group("Members age",function(){
    test.it("bye")
        .comment("good");
});

test.group( name ) — возвращает ссылку на группу, после чего её можно использовать как начало цепи, для добавления новой группы тестов или для добавления тестов в уже существующую подгруппу.
Вот последнее мы только что и сделали. Теперь в консоли видим:
good bye

И на последок, для закрепления всего выше сказанного:

полный листинг со всем выводом в консоль

console.log( // look how do test.typeof() work
    test.typeof(1)
   ,test.typeof("text")
   ,test.typeof([1,2,3])
   ,test.typeof({a:1,b:2})
   ,test.typeof()
   ,test.typeof(document)
   ,test.typeof(document.getElementsByTagName("body"))
   ,test.typeof(window)
   ,test.typeof(/yes it is RegExp/));

(function firstFunction() { // look how do test.trace() work
    (function secondFunction() {
        (function lastFunction() {
            console.log(test.trace());
        })();
    })();
})();

var Family = { // Here some complex object
    name: "Desiderio",
    pet: {
        type: "dog",
        name: "google"
    },
    members: [
        {
            name: "Titulus",
            age: 23
        },
        {
            name: "Dude",
            age: Infinity
        }
    ]
}
var myIQ = 100; // and value
var Nothing; // and empty value

test.it("hello world"); // Let"s add some simple tests
test.it(2+2==5).comment("i badly taught algebra at school"); // with comment
test.it(Infinity>Infinity-1).comment("philosophically is not it?"); // with expression
// check equalence
test.it(myIQ,"genious").comment("is I'm genious?");
test.it(myIQ,(1+10)*12 - 34 + 5*5*5 - 123).comment("check my IQ to be a normal");
// try some chain staff
if (test.it(Family).comment("Is Family exist? Is it not empty?").result()) {
    console.info("by if: ","Yep! Here it is!");
} else {
    console.warn("by if: ","ALARM! there are no Family");
}
// do it again in better way
test.it(Nothing).comment("Is Nothing exist? Is it not empty?").callback(
    function(){console.info("by callback: ","Yep! Here it is!");}
   ,function(){console.warn("by callback: ","ALARM! there are no Nothing");});

test.group("Empty group",function(){}); // try to make a group
test.group('Family tests',function(){ // let's unite it!
    test.it(Family.name,"Zukerberg").comment("Are we test Zukerberg's family?");
    test.it(Family.name,"Desiderio").comment("Or Desiderio's?");
}).comment("unite!");
test.group("Family tests",function(){ // and add some test after
    test.it(Family.pet).comment("Do we have a pet?")
        .callback(function(){
            // I add test in your test, so you can test while you testing
            test.it(Family.pet,{type:"dog", name:"google"}).comment("Is it right pet?");
        });
    test.it(Family.house).comment("Do we have a House?")
        .callback(function(){
            // next test will not be executed
            test.it(Family.pet,{type:"Huge", color:"green"}).comment("Is it right House?"); 
        });
});
test.group("here comes strange tests",function(){
    // test existance of most important Family properties
    test.them([Family.pet, Family.members])
        .comment("There must be memebers with pet, to call it a 'Family'");
    // test types of names
    test.types([Family.pet.name, Family.name],"string")
        .comment("Is names are string type");
    // here some magic
    var numberOfMembers = test.type(Family.members,"Array")
        .comment("Is it a several members, nor a single member?")
        .arguments()[0].length;
    test.it(numberOfMembers>5).comment("Is it big family?");
    // So if we know how many members there, lets check their age
    test.group("Members age",function(){
        for (i=0;i<numberOfMembers;i++) {
            test.it(Family.members[i].age>25)
                .comment("Is "+Family.members[i].name+" older then 25?");
        }
        test.it().comment("yep, here is error"); // add some error to see the trace
    });
});
// add final test deep in group
test.group("here comes strange tests").group("Members age",function(){
    test.it("bye").comment("good");
});

test.done();

full

root

Ах да. test.root всё ещё лежит на своём месте. Его всё также можно исползовать для создания новых вариантов отображения результатов. Он слегка упростился (у групп счётчики перестали разделять группы и тесты).
Пустой root выглядит так:

{
    "type": "group",
    "name": "root",
    "time": 0,
    "result": {
        "pass": 0,
        "fail": 0,
        "error": 0,
        "total": 0
    },
    "stack": []
}

Заключение

Очень хотелось бы поблагодарить:

  • camelos и zorro1211 за идеи цепочных вызовов
  • camelos отдельно за идею .callback
  • Anonym за идею доступа к группе извне. в том числе и из других файлов.

Всё ещё остались минусы приведённые в прошлой стате. Но уже есть весьма интересные мысли по методам их решений.

Сайт на котором можно увидеть весь приведённый выше код в действии, GitHub, Wiki

Автор: titulusdesiderio

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js