В первой части этой серии я шучу, что если бы это был производственный код, я бы написал тесты. Что ж, тесты - это то, что мне было нужно; в коде было несколько ошибок, которые проскочили. Мы возвращаемся к коду из части 2 и добавляем тесты для предметов в игре.
Для проведения тестов я буду использовать среду тестирования Jest (https://jestjs.io); его легко установить и создать чистую среду узлов. Есть много других фреймворков для тестирования, но я считаю Jest быстрым и простым в использовании.
Первый шаг - разбить код на отдельные файлы.
Во-первых, пункт.
// item.js const item = function( n ) { const _name = n; function name() { return _name; } return { name, isItem : true }; } module.exports = item;
Далее оружие.
// weapon.js const item = require( "./item"); const weapon = function( n, s ) { const _item = item( n ); const _strength = s || 1; function strength() { return _strength; } return { ..._item, strength, isWeapon : true } } module.exports = weapon;
Наконец, сумка.
// bag.js const item = require( "./item"); const bag = function( n, capacity ) { const _item = item( n ); const _size = capacity || 5; // 1 is the minimum size const _items = {}; function stash( item ) { if ( size() >= _size ) { return "Your bag is full, you have to drop an item first"; } if ( !item.isItem ) { return "A bag can only be filled with items not " + typeof item + "'s"; } _items[item.name()] = item; return null; } function drop( itemName ) { return fetch( itemName) } function fetch( itemName ) { let item = _items[itemName] if (item == null ) { return null; } delete _items[itemName]; return item; } function items( ) { return Object.keys( _items ); } function size() { return Object.keys( _items ).length } return { ..._item, stash, drop, fetch, items, size, isBag : true} } module.exports = bag;
Разделение на файлы не является обязательным требованием для тестирования; разделение тестов делает код более удобным для сопровождения в будущем.
Сначала напишем самый простой тест для item.js.
// item.test.js const item = require( "./item"); test( "Create a weapon and assert behavior", ()=>{ const myItem = item( "MyItem"); expect( myItem.name()).toBe("MyItem"); expect( myItem.isItem).toBe( true ); expect( myItem.isWeapon).toBeUndefined(); });
Код теста должен быть довольно очевидным; элемент не делает много. Мы могли бы добавить сюда еще несколько защитных тестов; например, что произойдет, если мы не укажем название для элемента? Можно ли добавить случайное имя? Кроме того, нет теста на предмет с другим именем, который будет обнаружен позже.
Далее мы добавляем тест для оружия.
// weapon.test.js const weapon = require("./weapon"); test( "Create a weapon and assert behavior", ()=>{ const obj = weapon( "MyWeapon"); expect( obj.name()).toBe("MyWeapon"); expect( obj.isWeapon).toBe( true ); expect( obj.isItem).toBe( true ); expect( obj.strength()).toBe( 1 ); }); test( "Create a powerful weapon", ()=>{ const obj = weapon( "LongSword", 5); expect( obj.name()).toBe("LongSword"); expect( obj.isWeapon).toBe( true ); expect( obj.isItem).toBe( true ); expect( obj.strength()).toBe( 5 ); });
Опять же, это достаточно ясный тестовый пример. Мы добавляем тест на прочность оружия и на то, что происходит, когда конструктору не предоставляется сила. Если вы сравните код в этой статье с кодом для оружия в последней, вы не заметите проверки на значение по умолчанию в предыдущей версии. Это одна из ценностей написания тестов.
Наконец, мы добавляем тест для сумки. Это намного сложнее.
// bag.test.js const bag = require("./bag"); const weapon = require("./weapon"); const item = require("./item"); test( "Create a bag and assert behavior", ()=>{ const obj = bag( "MyBag"); expect( obj.name()).toBe("MyBag"); expect( obj.isBag).toBe( true ); expect( obj.isItem).toBe( true ); expect( obj.isWeapon).toBeUndefined(); expect( obj.items()).toStrictEqual( [] ); expect( obj.size()).toBe( 0 ); }); test( "test stash", ()=>{ const obj = bag( "Bag of Holding", 2); expect( obj.name()).toBe("Bag of Holding"); const paper = item( "paper"); expect( obj.size()).toBe( 0 ); expect( obj.stash( paper )).toBeNull(); // Returns null on success, error message on fail expect( obj.size()).toBe( 1 ); expect( obj.items()).toStrictEqual( ["paper"]); expect( typeof obj.stash( "unknown")).toBe( "string" ); expect( obj.size()).toBe( 1 ); // Same item just succeeds expect( obj.stash( paper )).toBeNull(); expect( obj.size()).toBe( 1 ); const rock = item( "rock"); expect( obj.stash( rock )).toBeNull(); expect( obj.size()).toBe( 2 ); expect( obj.items()).toStrictEqual( ["paper", "rock"]); }); test( "test fetch", ()=>{ const obj = bag( "Bag of Holding", 2); expect( obj.name()).toBe("Bag of Holding"); const paper = item( "paper"); expect( obj.size()).toBe( 0 ); expect( obj.stash( paper )).toBeNull(); // Returns null on success, error message on fail expect( obj.size()).toBe( 1 ); expect( obj.items()).toStrictEqual( ["paper"]); expect( obj.fetch( "paper")).toBe( paper ); expect( obj.size()).toBe( 0 ); expect( obj.items()).toStrictEqual( []); expect( obj.fetch( "item")).toBeNull(); }); test( "capacity", ()=>{ const obj = bag( "Bag of little Holding", 1 ); const paper = item( "paper"); expect( obj.size()).toBe( 0 ); expect( obj.stash( paper )).toBeNull(); expect( obj.size()).toBe( 1 ); const rock = item( "rock"); expect( typeof obj.stash( paper )).toBe( "string"); expect( obj.size()).toBe( 1 ); });
Нам нужно проверить несколько крайних случаев на предмет размера сумки и того, что происходит, когда мы добавляем один и тот же товар дважды. Изначально мы выводили сообщение на консоль, когда что-то делали с сумкой. Печать значительно усложняет тестирование, поэтому мы переключаемся на возвращение значений известных ошибок для каждого вызова.
Это письмо короче, но мы надеемся, что оно поможет показать ценность тестирования и сделает код лучше.