Разработка через тестирование в JavaScript с использованием Qunit
Разработка через тестирование - это один из способов разработки программного обеспечения, который состоит из множества повторяющихся итераций, включающих:
- написание теста, покрывающего желаемые изменения
- написание кода, который пройдет тест
- проведение рефакторинга
Сегодня я хочу вам показать этот метод на примере JavaScript и QUnit.
Немного о QUnit
QUnit - это мощный, простой фреймворк для тестирования JavaScript. Его разрабатывают те же ребята, что делают jQuery. QUnit особенно полезен для интеграционного и регрессионного тестирования. Исходники можно взять на гитхабе http://github.com/jquery/qunit, а документацию почитать на http://api.qunitjs.com/
Для быстрого старта, создайте такую страницу:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="qunit/qunit.js"></script>
<script type="text/javascript">
module('Module 1');
test('test', function(){
ok(true);
});
</script>
</head>
<body>
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>
В итоге у вас должно получить нечто подобное
Техническое задание
Сделать селектбокс, с двумя кнопками "добавить" и "удалить"
- При нажатии на "добавить" селектбокс заменяется на текстовое поле, а кнопка "добавить" на "сохранить"
- При нажатии на "сохранить" содержимое поля добавляется пунктом в селектбокс, вид возвращается в исходное состояние
- При нажатии на "удалить" в режиме добавления возвращаться к исходному виду, в режиме отображение селектбокса удалять выбранный элемент, если список пуст показывать ошибку
- Селектбокс должен легко интегрироваться
Приступим
Мы имеем достаточно информации, так что вооружившись порядком действий из начала статьи и техническим заданием, приступим к выполнению. Первый пункт у нас "написание теста, покрывающего желаемые изменения". Определится с желаемыми изменениями нам поможет техническое задание. В нем есть пункт "Селектбокс должен легко интегрироваться", а это нас приводит к идеи о модульной структуре, опишем ее с помощью теста. Вернее сначала подготовим тестовую страницу, а затем напишем тест. Страница:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="qunit/qunit.js"></script>
<!-- Наш исходник -->
<script type="text/javascript" src="src/selectbox.js"></script>
<!-- Наш тест -->
<script type="text/javascript" src="tests/selectbox.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>
тест:
module('Selectbox');
test('Module structure', function(){
ok('selectbox' in window, 'selectbox not defined');
ok(typeof window.selectbox == 'Function', 'selectbox must be function');
});
запустим на выполнение и получим следующее:
Ура, тест успешно провален, а это значит что первый пункт нашего плана выполнен =) Переходим ко второму: "написание кода, который пройдет тест"
var selectbox = (function(){
return function(){};
})();
Запускаем тест, и получаем неожиданный результат. Он не прошел!
Давайте разбираться, первая часть теста пройдена, значит наш модуль экспортирован, но что же лежит в этой переменной? Функция! Но тест не пройден. Ответ кроется в нашем тесте, который написан с ошибкой =). Вы ведь заметили ее при первом прочтении? Тип нашего window.selectbox
на самом деле function
, а не Function
. Вот так вот, в тестах тоже могут быть ошибки, по этой причине их рекомендуется делать как можно проще. Что же, исправим и порадуемся результату.
Остался последний, очень важный пункт: проведение [[рефакторинга]]. Почему он важен? Мы собираемся писать высококачественный код, а одной характеристик такого кода является выдержанность стиля и соответствия стандартам (спецификациям, конвенциям, устным соглашениям).
var selectbox = (function(){
var selectbox = function(){}
return selectbox;
})();
Итерация номер 2
И теперь все повторятся, нам снова нужно написать тест. Определимся с желаемыми изменениями: мы хотим вывести селектбокс в нужный нам контейнер. Напишем тест:
test('Render', function(){
var wrapper = document.createElement('div');
var select = new selectbox();
select.render(wrapper);
ok(wrapper.childNodes.length > 0);
});
Этот тест не только не работает, но и бросает исключение, к счастью QUnit перехватывает его, и выводит как ошибку.
Пишем код js var selectbox = (function(document){ var selectbox = function(){ this._initDOM(); }
selectbox.prototype._initDOM = function() {
this.selectbox = document.createElement('selectbox');
this.buttonSave = document.createElement('button');
this.buttonSave.innerHTML = 'Save';
this.buttonDelete = document.createElement('button');
this.buttonDelete.innerHTML = 'Delete';
}
selectbox.prototype.render = function(wrapper) {
wrapper.appendChild(this.selectbox);
wrapper.appendChild(this.buttonSave);
wrapper.appendChild(this.buttonDelete);
}
return selectbox;
})(document);
После рефакторинга получим что-то похожее на это:
var selectbox = (function(document){
var selectbox = function(){
this._initDOM();
}
selectbox.prototype._initDOM = function() {
this.selectbox = document.createElement('selectbox');
this.buttonSave = document.createElement('button');
this.buttonSave.innerHTML = 'Save';
this.buttonDelete = document.createElement('button');
this.buttonDelete.innerHTML = 'Delete';
this.container = document.createElement('div');
this.container.appendChild(this.selectbox);
this.container.appendChild(this.buttonSave);
this.container.appendChild(this.buttonDelete);
}
selectbox.prototype.render = function(wrapper) {
wrapper.appendChild(this.container);
}
return selectbox;
})(document);
Итерация номер
Что бы вас не утомлять полным процессом, я просто приведу список изменений, который я хотел внести и результат, который получил. В качестве тренировки, вы можете сделать все сами, а затем сравнить с моим результатом. Не забывайте про 3 этапа, и не заскакивайте вперед. Помните - всему свое время =)
- При нажатии на "добавить" селектбокс заменяется на текстовое поле, а кнопка "добавить" на "сохранить"
- При нажатии на "сохранить" содержимое поля добавляется пунктом в селектбокс, вид возвращается в исходное состояние
- При нажатии на "удалить" в режиме добавления возвращаться к исходному виду
- При нажатии на "удалить" в режиме отображение селектбокса удалять выбранный элемент, если список пуст показывать ошибку
Результат /files/qunit/page.html
Тесты /files/qunit/test.html
PS: этот код не работает в IE, что можно легко выяснить с помощью тестов. Второе домашнее задание: сделать все кроссбраузерно.