Этот пост относится к работе 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 (который часто имеет большие зависимости и накладные расходы на установку), что я не могу до него добраться. Это упражнение позволило мне перейти к фактическому кодированию тестов без особых настроек и заставило меня немного больше подумать о структуре моего кода.