вторник, марта 28, 2006

Юнит-тесты рулят

Сегодня убедился, что юнит-тесты и в AS2 - вещь весьма полезная. Написал класс, к которому параллельно писал тесты. Не по методике TDD, но все же... И все тесты работают, а в приложении - нет. И тогда стал я писать тест, который показал бы мне таки красную полосу. И мне это удалось! После этого баг был локализован и выловлен. Баг оказался пустяковым - подумаешь по невнимательности в условной конструкции вместо == написал = :) Уж такого я на себя подумать не мог, да и со стороны все выглядело прилично. А потому стал думать а плясках с бубном, о собственной профнепригодности, о влиянии сложности приложения на нестабильно работающий класс, породившей баг, отладить который ввиду сложности приложения не представляется возможным. Не отчаивайтесь в моей ситуации - пишите тест. На кошках вылавливается легче.

Ну и под это дело как раз и хочу продемонстрировать пример. Но сначала, как известно, рабочая среда. Первым делом вам нужна библиотека AsUnit. Вам нужно скачать Framework. zip-архив содержит в себе три папки: as2, as25 и as3. Смысл примерно таков, что первая папка содержит все изначально написанную под AS2 библиотеку модульного тестирования. Пришло время AS3 и авторы переписали библиотеку путем портирования JUnit'а. А следом появилась бибилиотека AsUnit 2.5 - это портированная на AS2 библиотека версии AS3 :) В результате рекомендуется использовать 2.5. По сравнению с уже давно существующей AsUnit 2.0 там есть ряд преимуществ: в качестве тестов (единицы тестирования) считаются сами тестовые методы, а не assert'ы в методах, добавлены также улучшения по запуску тестов.

Так вот AsUnit 2.5 мы и используем в нашем проекте. Также понадобится компилятор mtasc, Apache Ant (о нем мы уже писали, а посему по вопросам установки отсылаю к соотвествующей статье) и задача Ant as2ant (на данный момент мною используется версия 1.5, просто копируем as2anr.jar в папку lib каталога с установкой Ant). После указания переменной окружения Path к компилятору mtasc можно считать рабочую среду подготовленной.

Приступим к самой теме. В качестве нашего предмета я выбрал некий абстрактный органайзер. Пускай у нас есть время, событие и некий флаг, показывающий, высок ли приоритет события или низок. Пример к жизни особого отношения не имеет - просто пример. Поэтому я позволи себе всякие вольности в его формулировке. Например, мы будем считать, что если мы планируем на одно и то же время сделать две вещи, то превращаем это в одно событие, а описания их просто конкатенируем. Но если они имеют разный приоритет, то и события разные. То есть предположим, на 14.00 я запланировал событие "Попить", "Поесть" и "Позвонить жене". И считаю, что приоритет первых двух невысок, а последнего - высок. Соотвественно я первые два объединяю в одно "Попить - поесть" с низким приоритетом, а последнее так и идет отдельно. То есть не факт, что я все это выполню, но если я посмторю, что у меня запланировано на 14.00, то увижу, что это два события: "Попить - поесть" и "Позвонит жене". Соотвественно хранит это дело у меня класс Organizer. В процессе разработки (через тестирование; это тема будущих статей и поэтому динамику разработки не показываю - только результат) у меня появился дополнительный класс Event для описания события. Его я тестировать не стал (а зря!) взяв на себя смелость утверждать, что он простой и тестирования класса Organizer вполне достаточно. Прежде чем привести результат, вкратце расскажу об устройстве модульных тестов. Модульный тест состоит, собственно, из теста. Тест - это некий метод некого тестового класса, тестирующий один из аспектов тестируемого класса. В нашем случае класс OrganizerTest будет содержать тесты (тестовые методы) для тестирования класса Organizer.

Класс OrganizerTest представляет из себя тестовый случай (Test Case) и потому расширяет класс TestCase. Класс TestCase устроен просто: он содержит в себе тесты (методы, начинающиеся со слова test), которые выполняются независимо. Перед выполнением каждого теста вызывается метод setUp, выполняющий некие начальные необходимые установки (создание экземпляра тестируемого класса, например). После вызова тестового метода вызывается метод tearDown (удаляет последствия). Результат каждого теста складывается из утверждений (assertion), которые либо выполняются, либо нет, а также исключений, которые могут выбрасываться тестируемым классом.

Несколько Test Cases объединяются в тестовый набор (Test Suite), для которых есть специальный класс (TestSuite). В нашем случае это AllTests, который содержит всего один тестовый случай. В тестовые наборы объединяют затем чтобы прогонять множество тестов сразу. При прогоне тестового набора автоматически прогоняются все тестовые набры, в него входящие (по шаблону Composite), а также все тестовые методы входящих Test Cases (определяются автоматически по началу на test).

Также в нашем проекте присутствует TestsRunner, запускающий тесты. Он выдает в качестве результата либо зеленую полосу (все тесты прошли удачно), либо красную - не сработал хотя бы один тест (утверждение, либо было выкинуто исключение). Красная полоса - это не плохо. Красная полоса - наш друг. Зеленая полоса зачастую заставляет задуматься, как будет показано дальше. Поэтому цель написания тестов - добиться красной полосы. Если полоса зеленая, то либо вы протестировали все, либо чего-то не протестировали :)

Также упомянем про наш build.xml. Этот ant-файл содержит две публичных цели: build и build.and.run. Цель build - цель по умолчанию. Она запускается при наборе команды ant без параметров в каталоге с билд-файлом. Она просто формирует билд нашего теста в виде swf-файла tests.swf в папке build. Цель build.and.run формирует билд и запускает его (надо указать правильный путь к flash-плееру в файле local.properties). Запускается просто: ant build.and.run.

Итак, наш класс Organizer:


/**
* @author Constantiner (constantiner at gmail dot com)
* @version Mar 27, 2006
*/

import constantiner.Event;

class constantiner.Organizer
{
private var events:Array;

/**
* Конструктор.
*/
public function Organizer ()
{
events = new Array ();
}

public function addEvent (eventDate : Date, eventDescription : String, isHiPriority:Boolean) : Void
{
var anEvent:Event = new Event (eventDate, eventDescription, isHiPriority);
var sameEvent:Event = getTheSame (anEvent);
if (sameEvent == null)
{
events.push (anEvent);
}
else
{
sameEvent.concat (anEvent);
}
}

public function getEventsNumber() : Number
{
return events.length;
}

public function getDescription (eventDate:Date, isHiPriority:Boolean):String
{
var anEvent:Event = new Event (eventDate, null, isHiPriority);
var sameEvent:Event = getTheSame (anEvent);
return sameEvent.getDescription ();
}

private function getTheSame (targetEvent:Event):Event
{
for (var i:Number = 0; i < events.length; i ++)
{
var tempEvent:Event = Event (events [i]);
if (targetEvent.equals (tempEvent))
{
return tempEvent;
}
}
return null;
}
}

Вспомогательный класс Event:


/**
* @author Constantiner (constantiner@gmail.com)
* @version Mar 27, 2006
*/

class constantiner.Event
{
private var eventDateMs:Number;
private var eventDescription:String;
private var isHiPriority : Boolean;

/**
* Конструктор.
*/
public function Event (eventDate:Date, eventDescription:String, isHiPriority:Boolean)
{
eventDateMs = eventDate.getTime ();
this.eventDescription = eventDescription;
this.isHiPriority = isHiPriority;
}

public function equals(targetEvent : Event) : Boolean
{
return Boolean ((targetEvent.isHiPriority == isHiPriority) && (targetEvent.eventDateMs = eventDateMs));
}

/**
* Метод, возвращающий строковую репрезентацию данного экземпляра (как правило, в целях отладки).
* @return Текстовая репрезентация данного экземпляра.
*/
public function toString() : String
{
var formattedDate:Date = new Date (eventDateMs);
return "constantiner.Event <date: " + formattedDate.toString() + ", isHiPriority: " + isHiPriority + ", eventDescription: " + eventDescription + ">";
}

public function concat(targetEvent:Event) : Void
{
eventDescription += " --- " + targetEvent.eventDescription;
}

/**
* @param
* @return
*/
public function getDescription ():String
{
return eventDescription;
}
}

Теперь наш тест:


/**
* @author Constantiner (constantiner@gmail.com)
* @version Mar 27, 2006
*/

import asunit.framework.TestCase;
import constantiner.Organizer;

class constantiner.tests.OrganizerTest extends TestCase
{
private var className:String = "constantiner.tests.OrganizerTest";
private var instance:Organizer;

public function setUp():Void
{
instance = new Organizer ();
}

public function tearDown():Void
{
delete instance;
}

public function testInstantiated():Void
{
assertTrue("target instantiated", instance instanceof Organizer);
}

public function testAddDate ():Void
{
instance.addEvent (new Date (1999, 5), "Hello event", true);
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть одно событие, а реально их " + eventsNumber, eventsNumber, 1);
instance.addEvent (new Date (2000, 5), "Next year hello", false);
eventsNumber = instance.getEventsNumber ();
assertEquals ("Должно быть два события, а реально их " + eventsNumber, eventsNumber, 2);
}

public function testAddTheSameDate ():Void
{
var firstDate:Date = new Date (2001, 5);
var secondDate:Date = new Date (firstDate.getTime());
instance.addEvent (firstDate, "Hello event", true);
instance.addEvent (secondDate, "And hello again", true);
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть одно событие, собранное из двух, а реально их " + eventsNumber, eventsNumber, 1);
}

public function testConcatDescription ():Void
{
var firstDate:Date = new Date (2001, 5);
var secondDate:Date = new Date (firstDate.getTime());
var firstDescription:String = "Hello event";
var secondDescription:String = "And hello again";
instance.addEvent (firstDate, firstDescription, true);
instance.addEvent (secondDate, secondDescription, true);
var newDescription:String = instance.getDescription (firstDate, true);
assertTrue ("Новое описание события должно быть длиннее их суммы. Новое описание: " + newDescription, (newDescription.length > (firstDescription.length + secondDescription.length)));
}
}

Запускаем наш Test Runner и видим зеленую полосу:

AsUnit 2.5 by Luke Bayes and Ali Mills
....

Time: 0.026

OK (4 tests)

Ура, говорим мы, наш класс работает! Внедряем его в проект - и находим ошибки (а вы видите ошибку в классах?). Но такого не может быть! Мы же все оттестировали! Обманчива эта иллюзия. Можно подумать, что окружающий код влияет пагубно, ошибку обнаружить нельзя итд. Но на самом деле хватит мумить! Только не мумить! Нам надо сломать тест! Красная полоса - наше спасение! Приступим!

Собственно я написал в итоге темт, дающий красную полосу. Вот он:


public function testMultiplyAdd ():Void
{
var count:Number = 10;
for (var i:Number = 0; i < count; i ++)
{
var firstDate:Date = new Date (2001 + i, 5);
var secondDate:Date = new Date (firstDate.getTime());
var firstDescription:String = "Hello event";
var secondDescription:String = "And hello again";
instance.addEvent (firstDate, firstDescription, true);
instance.addEvent (secondDate, secondDescription, false);
}
var eventsNumber:Number = instance.getEventsNumber ();
assertEquals ("Должно быть " + count * 2 + " событий, а реально их " + eventsNumber, count * 2, eventsNumber);
}
}

Запускаем и видим:


AsUnit 2.5 by Luke Bayes and Ali Mills
.....F

Time: 0.028
There was 1 failure:
0) constantiner.tests.OrganizerTest.testMultiplyAdd()
assertEquals.message: Должно быть 20 событий, а реально их 2

FAILURES!!!
Tests run: 5, Failures: 1, Errors: 0

Красная полоса! Ура! Дальше можно написать тесты еще, можно написать тесты для класса Event. Но в таком простом и изолированном случае, каким является наш тест, мне проще оказалось найти методом пошаговой отладки тупой баг по невнимательности в методе equals класса Event. Сравнение - это == , а не =. Будьте внимательны!

Ну вот, собственно, мой первый урок использования модульного тестирования в AS2-разработке. Скажу, что статья далеко не полная и не охватывает важного аспекта модульного тестирования - методики TDD (разработка через тестирование). Но все еще впереди!

Ну а материалы к статье можно скачать тут. В архиве все исходники, билд-файл, а также библиотека AsUnit 2.5. То есть все готово к запуску теста и экспериментам. А в качестве решения проблем при скачивании файла могу посоветовать указать ссылку на этот файл в качестве реферера.

Удачных открытий!

Комментариев 20:

Anonymous Анонимный сообщает:

Браво. Глядишь, скоро олд-скульные товарищи совсем раскачаются...

29 марта, 2006 00:34  
Anonymous Анонимный сообщает:

Вряд ли скоро раскачаются. Как много, скажем, Java-программеров пользуют TDD? Точных данных у меня, конечно, нет, но чувствую, что не так много. А ведь это, по идее, наиболее прогрессивные программеры. Что уж говорить про "олд-слульных экшонскриптеров"...
Однако, TDD лично мне действительно часто помогало, и не только избежать ошибок, но и в процессе разработки, лучше продумываются интерфейсы и т.д. Так что автору - респект, тем кто не знает, что такое Юнит-тесты и TDD - учите, пробуйте, оно действительно помогает.

29 марта, 2006 00:56  
Anonymous Анонимный сообщает:

Автору респект :) Как раз сегодня перечитывая в очередной раз "Java без сбоев", дочитав про TDD, думал где бы найти подобное для АС2...

29 марта, 2006 01:17  
Blogger Unknown сообщает:

2All
Ну я очень рад, что тема актуальна. Глядишь, двинем в массы :)

29 марта, 2006 09:49  
Anonymous Анонимный сообщает:

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

Востребованность тестов (да и AS2-3 в целом) прямо пропорциональна количеству больших (читай: сложных) проектов на Flash.
Сегодня 80-90% проектов на Flash не требуют знания AS вообще, либо требуют на минимальном уровне.
Но.
Ситуация меняется. Возрастает доверие инвесторов к проектам на Flash, появляются новые инструменты, растут специалисты.
Это общая тенденция и это радует: без хлеба не останемся :).

29 марта, 2006 10:48  
Anonymous Анонимный сообщает:

Тесты конечно рулят и тесты это есть благо и т.д. и ТДД ...
Но, если кто-то придумает, как юнит тестированием тестировать интерфейс я на этого чувака буду молится.

29 марта, 2006 11:42  
Anonymous Анонимный сообщает:

Класс! Спасибо за статью. Теперь я приблизительно начинаю понимать, что это за тесты такие.

И все же у меня вопросы по собственно их надобности.. То есть, мы здесь имеем что - пара весьма простых классов и тестовый класс по объему почти сравнимый с самими тестируемыми классами.. Насколько полны должны быть тестовые наборы? В данном случае понятно - с первым набором у тебя получилась зеленая полоса, но компилятор ругнулся - знач надо тесты добавлять. Это есть критерий полноты? То есть, тесты нужно писать, только если есть ошибки - для отладки?
Если для отладки, то не проще ли воспользоваться трейсами в самих классах?
Хотя, наверно, тесты хороши, чтобы провести изолированное тестирование, исключив влияние приложения на класс. Но всегда ли можно класс изолировать?

29 марта, 2006 13:30  
Blogger Unknown сообщает:

2Iv
Я тоже надеюсь, что Flash-технология приподнимется :))

29 марта, 2006 16:36  
Blogger Unknown сообщает:

2Mokus
Методики есть (в основном, конечно, у старших братьев - Java, C#). И я как раз и собираюсь их изучить и попробовать применить к нашей области. А, может, кто-нибудь меня опередит :)

29 марта, 2006 17:43  
Blogger Unknown сообщает:

2Fix
Так ты как раз сразу пишешь тест изолированного твоего класса! Соотвественно и отладка этого класса будеит делом существенно более легким. Мало того, ты пишешь тесты класса еще до того, как создаешь тестируемый функционал. Казалось бы тестирующий код имеет тот же объем, что и тестируемый. И разработка должна замедлиться. Но тут есть несколько моментов, которые как раз ускоряют разработку. Во-первых, храбрость. Ты не боишься кода, у тебя не опускаются руки. Твои тесты всегда дают тебе уверенность. Во-вторых, тесты являются средством документирования твоих классов. Вместо того чтобы объяснять кому-то применение твоего кода, писать примеры, вводить людей в курс дела, ты показываешь им тесты - они должны быть максимально просты и понятны даже самому разработчику. Далее, тесты позволяют вносить изменения в программный код. Часто у тебя было, что ты вносил изменения в одном месте и где-то что-то ломалось? Или, например, ты просто боялся вносить изменения в оттестированное нелегким дебагом приложение потому что не был уверен, что что-то где-то не сломается и опять дебажить неизвестно что неизвестно где? Тесты дают тебе возможность запускать себя после каждого шага (они должны быть легкими) и убеждаться, что все по-прежнему работает так, как ты и предполагал. Или не работает, но ты сразу увидишь, что не работает и где. Это тоже экономит время и дает возможность вносить изменения в программу.
Есть куча и других преимуществ. А трэйсы... Часто ли ты запутывался в кучах трэйсов (они ведь выводят все- и там где все хорошо, и там где не хорошо)?
А писать тесты надо когда в тестируемый класс добавляется новый функционал - для тестирования этого функционала. Если у твоего класса есть некая особенность - ты пишешь тест чтобы показать (задокумментировать) эту особенность. Ну и, понятно, если ошибки все еще есть - после их отлова ты пишешь тест, который проверяет эту отловленную ошибку.
Насчет покрытия кода тестами, написания тестов к уже готовому коду итд., я бы отослал тебя к первоисточнику - книге Кента Бека. Жаль, что тиражи уже распроданы, но по секрету могу дать адресок, где ее еще можно купить :)
Ну и как говорит сам Кент Бек (автор TDD и один из авторов JUnit), он иногда запускает тесты даже не внося никаких изменений в код - для уверенности :) И я с ним согласен :)))

29 марта, 2006 18:00  
Anonymous Анонимный сообщает:

2Mokus
То о чем Вы говорите называется "функциональное тестирование", посмотрите http://osflash.org/autotestflash, очень сыро но можно эту тему развивать, до бесконечности.

29 марта, 2006 21:12  
Blogger Unknown сообщает:

2mrjazz
В том-то и дело, что тестирование GUI не всегда является функциональным тестированием. Функциональное тестирование - это тестирование функциональных требований. Если, скажем, я тестирую диалог, я, например, хочу, запустив тесты, убедиться, что кнопка Ok при нажатии генерирует событие MyDialog.OK_EVENT, а кнопка Cancel - MyDialog.CANCEL_EVENT. Тестирование данного факта никоим образом функциональным тестированием не является, однако тестировать и зафиксировать в тестах подобные вещи очень часто хочется. GUI имеет свойство меняться, и если завтра придет менеджер проекта (у вас есть проджект-менеджеры? у нас - нет :( ) и скажет - хочу здесь поменять, кнопку эту вынести туда, эту сюда, то тест поможет вам получить работающее приложение после таких изменений.

29 марта, 2006 22:43  
Anonymous Анонимный сообщает:

спасибо за очередную статью по теме!

----------------------
в проблемах Eclipce (fdt)
пишет, что не может найти "org.as2lib.ant.Mtasc",
при этом всё запускается и работает.
так и должно быть?

31 марта, 2006 13:41  
Anonymous Анонимный сообщает:

Насчет интерфейса понятно, а вот как протестировати такую штуку как конвеер Евгения Потапенко?
Т.е. класс - черный ящик, интерфейс обозначен четко, а вот как его протестировать? Ведь результат "растянут во времени", а это очень типично для флеш приложений.

31 марта, 2006 13:51  
Blogger Unknown сообщает:

2agahov
Думаю, что ты в настройках Ant в Eclipse не указал соотвествующий jar. Можно указать как в глобальных настройках, так и в настройках запуска целей самого билд-файла.

31 марта, 2006 20:59  
Blogger Unknown сообщает:

2mrjazz
Ну в AsUnit есть средства тестирования асинхронных вызовов.

31 марта, 2006 21:00  
Anonymous Анонимный сообщает:

если не указать jar в настройках ant, задача ant выдает ошибку под eclipce;
---------------------------------
при указания jar в настройках билд-файла у меня при запуске eclipce выдается предупреждение :(
задача ant запускается без ошибок
--------------------
при указания jar в глобальных настройках ant всё ок!

01 апреля, 2006 00:16  
Anonymous Анонимный сообщает:

А как насчет того, чтоб при непрохождении тестов валился билд???

12 апреля, 2006 16:28  
Blogger Unknown сообщает:

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

12 апреля, 2006 21:20  
Anonymous Анонимный сообщает:

Само желание чтобы НАС ценили, и любили, уже показывает наше неправильное положение.
Дело в том, что, если мы думаем, о любви, которая направлена только на нас, то это не что иное, как ЭГОИЗМ. Я думаю только о себе, чтобы «Я» был любим, «Я» был, ценим и уважаем, но природа любви совершенно иная.А вот мой сайт возможно вас за интересует это - очередной островок любви и нежности - buy viagra ;);)...Большое спасибо вам и я очень благадарна вам за визит на мой сайт ...удачи...С Уважением Лариса

14 июля, 2010 01:02  

Отправить комментарий

Вернуться на главную