понедельник, 19 августа 2013 г.

Про TypeScript и Unit-test'ы

Недавно для своего opensource-проекта CamlJs.codeplex.com (aka SharePoint EcmaScript Caml Builder) потребовалось написать юнит-тесты. При этом раскопал несколько приятных полезностей, которыми собственно и хочу поделиться в этой статье.

CamlJs


На всякий случай, для тех кто возможно не в курсе, суть этого проекта - удобное формирование XML Caml-запросов для SP.CamlQuery и/или для SPServices, с интеллисенсом и подсказками. Выглядит это примерно так:




Особенность проекта в том, что он действительно не только бережет от опечаток, но практически основной целью при создании CamlJS являлось создание качественного интеллисенса и подсказок, чтобы не требовалось быть экспертом в Caml Query чтобы написать средний запрос.

Проект, даже на самой ранней стадии, представлял довольно внушительную гору JavaScript, довольно тяжеловесную в поддержке. В общем, переход на TypeScript был очень логичным шагом и значительно облегчил поддержку проекта и его дальнейшую разработку. Собственно благодаря переходу на TypeScript какая-то дальнейшая работа и стала возможной - сейчас как раз веду работы по новому синтаксису и готовлюсь релизить... :)

При всём этом, проект практически идеален для юнит-тестов: XML-запросы, которые должен генерировать билдер - это просто строки, которые очень легко проверить. Сначала я проводил тестирование "кустарно" - написал файлик и напулял туда простых сравнений. Конечно же со временем этого стало не хватать и проект дозрел до введения уже нормальных юнит-тестов.

tsUnit


Простенький движок для юнит-тестов на TypeScript существует - это tsUnit. Пользоваться примерно так: наследуемся от класса tsUnit.TestClass, используем методы класса-родителя для assert'ов, и плюс потребуется простенький код инициализации.

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

/// <reference path="tsUnit.ts" />
/// <reference path="Calculations.ts" />

class SimpleMathTests extends tsUnit.TestClass {

    private target = new Calculations.SimpleMath();

    addTwoNumbersWith1And2Expect3() {
        var result = this.target.addTwoNumbers(1, 2);

        this.areIdentical(3, result);
    }

    addTwoNumbersWith3And2Expect5() {
        var result = this.target.addTwoNumbers(3, 2);

        this.areIdentical(4, result); // Deliberate error
    }
}

// new instance of tsUnit
var test = new tsUnit.Test();

// add your test class (you can call this multiple times)
test.addTestClass(new CalculationsTests.SimpleMathTests());

// Use the built in results display
test.showResults(document.getElementById('results'), test.run());

Неудобства


Однако мне хотелось заточить движок под CamlJs так, чтобы с тестами было работать проще, и в случае проваленного теста было бы проще найти ошибку. Я видел следующие 2 основных неудобства при работе с тестами:

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

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

Идеальным решением в этом случае была бы, безусловно, подсветка отличий (diff).

Давайте рассмотрим, как я решил эти проблемы.

Сравнение форматированного и неформатированного XML в JavaScript


После нескольких попыток, я предпочел решить проблему хранения XML-простыней в коде тестов путем перегонки обоих кусков XML в объекты и затем в JSON через JSON.stringify и сравнения уже JSON-строк. Таким образом, я смогу хранить XML-фрагменты в коде тестов в форматированном виде, что улучшит читабельность кода.

Когда-то давно я писал про то, что в SharePoint есть встроенная функция для превращения XML-строки в dom element. Эта функция прекрасно работает, однако оказалось, что получившийся dom-объект нельзя сериализовать через JSON.stringify (вообще никакие dom-элементы через JSON.stringify не выводятся)! Готового решения этой проблемы я не нашел, но быстро написал простенькую функцию на основе фрагмента из интернетов, которая dom-элемент перегоняет в объект JavaScript. Функция эта выглядит следующим образом:

        function elementToObject(el) {
            var o = {};
            var i = 0;
            if (el.attributes) {
                for (i; i < el.attributes.length; i++) {
                    o[el.attributes[i].name] = el.attributes[i].value;
                }
            }

            var children = el.childNodes;
            if (children.length) {
                i = 0;
                for (i; i < children.length; i++) {
                    if (children[i].nodeName == '#text')
                        o['#text'] = children[i].nodeValue;
                    else
                        o[children[i].nodeName] = elementToObject(children[i]);
                }
            }
            return o;
        }


Дальше элементарно:

var domElement = CUI.NativeUtility.createXMLDocFromString("<root>" + xml + "</root>");
var obj = elementToObject(domElement);
return JSON.stringify(obj.root, undefined, 2);

В результате XML из скриншота выше преобразуется вот в такой кусок более ёмкого и читабельного JSON'а:

 {
   "Where":  {
     "And":  {
       "In":  {
         "FieldRef":  {
           "Name": "Category"  },
         "Values":  {
           "Value":  {
             "Type":  "Integer",
             "#text":  "10"
           }
         }
       },
       "Leq":  {
         "FieldRef":  {
           "Name":  "ExpirationDate"
         },
         "Value":  {
           "Type":  "Date",
           "Now":  {}
         }
       }
     }
   },
   "OrderBy":  {
     "FieldRef":  {
       "Name":  "ExpirationDate",
       "Ascending":  "False"
     }
   }
 }

Обратите внимание: неважно, что этот JSON не идеальный. Нет задачи сделать идеальный JSON, есть задача сравнить 2 XML'а.

Итоговый код моих юнит-тестов для проекта CamlJs (включая хелпер для реализации преобразования XML->Json) можно найти на Codeplex.

Возвращаясь к неудобствам: даже имея результаты выполнения в форматированном JSON, сравнение запросов в случае проваленного теста по прежнему являлось серьезной проблемой, т.к. запросы большие и приходилось постоянно скроллить туда-сюда, пытаясь найти отличия. Так что давайте теперь рассмотрим, как я сделал diff :)

Визуальный Diff на JavaScript

На самом деле, тут всё банально, есть готовая библиотека, которая делает diff на js. Мне пришлось только немного пропатчить tsUnit, чтобы обеспечить интеграцию. В итоге получилась вот такая картинка:


Сразу видно, что происходит, удобно, работа ускорилась в разы.

Библиотека не идеальная и ищет отличия в словах (т.е. если у вас текст без пробелов, толку от неё мало). Лично мне хватило, т.к. я использую форматирование JSON и как следствие, везде где надо пробелы стоят. Однако есть более крутые библиотеки, и их довольно много. В частности, есть очень хорошая либа google-diff-match-patch от Neil Fraser, я на неё поздновато наткнулся, но алгоритм там более подходящий для моих целей, и библиотека сама очень развитая.

Выводы

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

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

С другой стороны, очень важно здесь не переусердствовать. Нельзя писать систему чтобы писать систему :) Есть простое решение проблемы - отлично, применяем быстро, и продолжаем работать над основным проектом. Быстро не получилось? Значит без этого "удобства" можно обойтись.

Так что, удобной разработки, и до скорых встреч!

4 комментария:

  1. ты его, кстати, в nuget не публикуешь?

    ОтветитьУдалить
    Ответы
    1. щас нет вроде, но в готовящемся релизе 2.0 это будет обязательно

      Удалить
  2. Офигенный пост, спасибо, Андрей!

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.