Этот пост относится к работе 7 августа, что было 54-м днем ​​моего #100daysofcode

Итак, этим утром я открыл еще один проект, который великолепно свободен от тестов, и приступил к добавлению в него нескольких тестовых случаев. В этом проекте есть много «модулей», которые довольно легко создаются и используются без каких-либо зависимостей от запуска в браузере, поэтому они гораздо лучше подходят для модульного тестирования. Я понимаю, что код, который я пытался протестировать вчера, был скорее интеграционным, чем модульным.

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

Установка

У меня есть два стиля объектов в этом проекте: те, которые создаются только один раз, объявляются как литералы объектов, а те, которые могут создаваться более одного раза, объявляются как функции с методами, прикрепленными к прототипу. Я сделал это потому, что, насколько я понимаю, в javascript объектный литерал, который создается более одного раза, неэффективен в памяти: все внутренние функции создаются снова и снова, когда вы штампуете копию, а внутренние функции не прикрепленный к прототипу, вы не можете изменить все эти функции одновременно, изменив прототип, вам придется отслеживать это самостоятельно. Я не уверен, что хочу или мне нужно будет сделать последнее, но я, безусловно, хочу, чтобы память была эффективной, если это возможно.

Я чувствую, что для того, чтобы использовать их с Mocha, я собираюсь сделать из них настоящие модули. Итак, я начну со статических объектов, которые будут простыми:

var Armors = {
    armors: [
        {"armorDamageModifier": 1, "armorName": "Plain Clothes"},
        {"armorDamageModifier": 1.1, "armorName": "Cheap Leather"},
        {"armorDamageModifier": 1.2, "armorName": "Sturdy Leather"},
        {"armorDamageModifier": 1.3, "armorName": "Hardened Leather"},
        {"armorDamageModifier": 1.4, "armorName": "Rigid Linen"}
        ]
    ,getArmorDamageModifier: function(armorID){
        return this.armors[armorID].armorDamageModifier;
    }
    ,getArmorName: function(armorID){
        return this.armors[armorID].armorName;
    }
};

Чтобы обновить это до модуля, который можно загрузить в тестовых сценариях на основе NodeJS, все, что мне нужно добавить, это:

if (typeof module!='undefined' && module.exports){
    module.exports = function(){
        return Armors;
    };
}

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

Я немного покопался и понял, что есть гораздо более простой шаблон:

if (typeof module!='undefined' && module.exports){
    module.exports = Armors;
}

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

Наряду с Mocha я использую shouldjs, потому что я использовал его вчера, и, похоже, в нем есть все, что мне нужно. Поэтому я требую свои зависимости и проверяю правильность создания экземпляра объекта Armors, регистрируя его в консоли:

var Armor = require('../Armor');
var should = require('should');
console.log(Armor);

Что выводит:

{ armors: 
 [ { armorDamageModifier: 1, armorName: ‘Plain Clothes’ },
 { armorDamageModifier: 1.1, armorName: ‘Cheap Leather’ },
 { armorDamageModifier: 1.2, armorName: ‘Sturdy Leather’ },
 { armorDamageModifier: 1.3, armorName: ‘Hardened Leather’ },
 { armorDamageModifier: 1.4, armorName: ‘Rigid Linen’ } ],
 getArmorDamageModifier: [Function],
 getArmorName: [Function] }

Отличная штука, наконец-то есть что протестировать.

Тесты

Я не специалист по DevOps, но я работал в компаниях с отделами и людьми DevOps. А еще я работал с некоторыми хорошими разработчиками, которые тестируют свой код. Я не уверен на 100%, что именно я должен тестировать на данном этапе, но я знаю, что если я тестирую что-то, это должно быть лучше, чем ничего не тестировать.

Глядя на мой объект "Броня", я думаю, что мне следует протестировать следующее, чтобы убедиться, что все существует:

  • Имеет пять объектов в массиве доспехов.
  • У него есть функция getArmorDamageModifier.
  • у него есть функция getArmorName

Базовая внешняя оболочка будет довольно простой, мне не нужны какие-либо шаги по настройке или демонтажу, поэтому следуйте документам Mocha, которые у меня есть:

describe('Armor', function(){
    ...
});

Я не уверен, как лучше всего структурировать операторы describe(), поскольку документы Mocha, как правило, имеют несколько вложенных операторов describe(). Я просто собираюсь придерживаться одного для каждого объекта в минуту.

Итак, я добавляю и блокирую:

it('should have an array of armors with length of 5 armors', function(done){
    ...
});

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

Проконсультируйтесь с документами модуля shouldjs для утверждений, которые я ищу

  • как проверить, что что-то является массивом.
  • как проверить длину указанного массива.

Синтаксис очень приятный и почти читаемый человеком:

Armor.armors.should.be.a.Array;
Armor.armors.should.have.lengthOf(5);

Отличные вещи, за исключением того, что я забыл вызвать ‘done()’, так что время моего теста истекло. Готовый начальный тест:

describe('Armor', function(){
    it('should have an array of armors with length of 5 armors', function(done){
        Armor.armors.should.be.a.Array;
        Armor.armors.should.have.lengthOf(5);
        done();
    });
});

Два других свойства — это функции, и, конечно же, shouldjs может проверять функции так же, как и массивы:

it('should have a function of getArmorDamageModifier', function(done){
    Armor.should.have.property('getArmorDamageModifier');
    Armor.getArmorDamageModifier.should.be.a.Function;
    done();
});
it('should have a function of getArmorName', function(done){
    Armor.should.have.property('getArmorName');
    Armor.getArmorName.should.be.a.Function;
    done();
});

Пока все очень просто, хотя в данный момент немного многословно.

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

  • можно вызвать getArmorDamageModifier для каждого элемента в массиве доспехов.
  • можно вызвать getArmorName для каждого элемента в массиве доспехов.
  • getArmorDamageModifer возвращает число.
  • getArmorName возвращает строку.

Итак, вернемся к простому старому javascript для цикла, выполните тесты внутри цикла, вызовите done(), когда цикл завершится:

it('should have a property of armorDamageModifier on every object which returns a number', function(done){
    for(var item in Armor.armors) {
        Armor.armors[item].should.have.property ('armorDamageModifier');
        Armor.getArmorDamageModifier(item).should.be.a.Number;
    }
    done();
});
it('should have a property of armorName on every object which returns a string', function(done){
    for(var item in Armor.armors) {
        Armor.armors[item].should.have.property('armorName');
        Armor.getArmorName(item).should.be.a.String;
    }
    done();
});

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

Нестатические объекты

Примечание. Возможно, я не на 100% правильно использую статические и нестатические данные, и заранее приношу свои извинения за незнание.

Предыдущий пример обращается к объекту, который, как мне кажется, является статическим, поскольку он всегда возвращает одно и то же, если вы вызываете его с одинаковыми параметрами, например, Armors.armors[0].getArmorName() всегда будет возвращать «Простая одежда». Это также литерал объекта, возвращаемый как часть модуля. Некоторые из моих других объектов — это функции с методами, прикрепленными к их прототипу. Раньше, когда я использовал module.exports = function(){return something}, я пытался протестировать свои объекты-функции как модули, но попал в правильное состояние. Именно это привело меня к поиску шаблона module.exports = something . Сейчас я даже не могу вспомнить странность, с которой я столкнулся, пытаясь сделать эти функции доступными для тестирования, но теперь я разобрался.

Итак, для самого маленького из них у меня есть:

function rpgConsole(id) {

    this.content = [];
    this.writeCount = 0;
    this.id = id;
    this.output = '';

}

rpgConsole.prototype.log = function (theClass, theString) {
    this.writeCount = this.content.unshift({"theClass": theClass, "theText": theString});
    var outputTemplate = '<p class="{theClass}">{theText}</p>';
    for (var a in this.content[0]) {
        outputTemplate = outputTemplate.replace('{' + a + '}', this.content[0][a]);
    }
    this.output = outputTemplate + this.output;
};

rpgConsole.prototype.display = function () {
    document.getElementById(this.id).innerHTML = this.output;
};

И добавив:

if (typeof module != 'undefined' && module.exports) {
    module.exports = rpgConsole;
}

Внизу я могу использовать его как модуль, экземпляр которого создается с помощью ключевого слова new. Я не утверждаю, что это лучше всего, просто мне не нужно менять целую кучу кода, чтобы протестировать его.

Я добавляю конструктор в свой тест внутри блока describe, но перед любыми предложениями it:

var rc = new rpgConsole('test');

Сначала я решил проверить, что созданный объект имеет значения по умолчанию. Единственное отличие от любого кода выше состоит в том, что некоторые из этих свойств имеют начальные значения. shouldjs примет второй параметр для функции .property(), который довольно удобно использовать для проверки свойства:

it('should have a property of id which is set to the string "test"', function(done){
    rc.id.should.be.a.String;
    rc.should.have.property('id', 'test');
    done();
});

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

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

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

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

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

Тест:

it('should have a write count of 1  and a content length of 1 if the log function is executed', function(done){
    rc.log('theClass', 'theString');
    rc.should.have.property('writeCount', 1);
    rc.content.should.have.lengthOf(1);
    done();
});

Метод вызывается для объекта и проверяются новые значения. Я начинаю немного смешивать вещи здесь, я вызвал метод, который воздействует на 3 разных объекта, но я тестирую только два из них. У меня также есть эти два теста после этого:

it('should have a content item with a property "theClass" which is set to "theClass" and a property of "theText" which is set to "theString"', function(done){
    rc.content[0].should.have.property('theClass', 'theClass');
    rc.content[0].should.have.property('theText', 'theString');
    done();
});
it('should have a property output which is set to <p class="theClass">theString</p>', function(done){
    rc.output.should.equal('<p class="theClass">theString</p>'); 
    done();
});

Вторые два теста пытаются проверить то, что было настроено в первом тесте. Если первый тест не пройден из-за того, что метод не работает, то они автоматически не пройдут. Мне нужно будет реорганизовать их для надежности и общего здравого смысла, даже поместить все это в один и тот же блок it было бы лучше, но тогда мое описание теста может стать очень длинным. Я вернусь к этому после некоторого дальнейшего чтения и добавлю несколько вложенных описаний, которые проверяют структуру объекта отдельно от функциональности объекта.

Заключение

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