31.05 2011

Разработка через тестирование - это один из способов разработки программного обеспечения, который состоит из множества повторяющихся итераций, включающих:

  • написание теста, покрывающего желаемые изменения
  • написание кода, который пройдет тест
  • проведение рефакторинга

Сегодня я хочу вам показать этот метод на примере 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>

В итоге у вас должно получить нечто подобное

JQuery test suite

Техническое задание

Сделать селектбокс, с двумя кнопками "добавить" и "удалить"

  1. При нажатии на "добавить" селектбокс заменяется на текстовое поле, а кнопка "добавить" на "сохранить"
  2. При нажатии на "сохранить" содержимое поля добавляется пунктом в селектбокс, вид возвращается в исходное состояние
  3. При нажатии на "удалить" в режиме добавления возвращаться к исходному виду, в режиме отображение селектбокса удалять выбранный элемент, если список пуст показывать ошибку
  4. Селектбокс должен легко интегрироваться

Приступим

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

<!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');
});

запустим на выполнение и получим следующее:

QUnit failed. Yeep, it work

Ура, тест успешно провален, а это значит что первый пункт нашего плана выполнен =) Переходим ко второму: "написание кода, который пройдет тест"

var selectbox = (function(){
    return function(){};
})();

Запускаем тест, и получаем неожиданный результат. Он не прошел!

Test failed. Nooooooo

Давайте разбираться, первая часть теста пройдена, значит наш модуль экспортирован, но что же лежит в этой переменной? Функция! Но тест не пройден. Ответ кроется в нашем тесте, который написан с ошибкой =). Вы ведь заметили ее при первом прочтении? Тип нашего window.selectbox на самом деле function, а не Function. Вот так вот, в тестах тоже могут быть ошибки, по этой причине их рекомендуется делать как можно проще. Что же, исправим и порадуемся результату.

QUnit, one half path

Остался последний, очень важный пункт: проведение [[рефакторинга]]. Почему он важен? Мы собираемся писать высококачественный код, а одной характеристик такого кода является выдержанность стиля и соответствия стандартам (спецификациям, конвенциям, устным соглашениям).

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 &gt; 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, что можно легко выяснить с помощью тестов. Второе домашнее задание: сделать все кроссбраузерно.

comments powered by Disqus