По работе как-то потребовалось добавить функциональность в один самописный flex-компонент. При этом важно было не поломать уже существующее поведение, т.к. компонент за время своего существования был использован в нескольких приложениях и оброс наследниками.
Стандартный подход к решению подобных задач — начать с написания юнит-тестов, покрывающих нынешнее поведение компонента, попутно проясняя для себя особенности его устройства.
Только после этого можно начинать пошаговый рефакторинг и расширение функциональности, постоянно прогоняя тесты на предмет не сломалось ли чего в результате изменений. [3]
Задача однако осложняется тем, что визуальные компоненты во флекс имеют многофазную асинхронную процедуру инициализации и обновления свойств, и для них требуется особые средства для написания тестов. FlexUnit 4 позволяет легко справляться с этой задачей, и ниже я покажу как это делать, а заодно и раскрою пару нюансов.
При тестировании наследников UIComponent мы должны учитывать, что большинство свойств компонента находятся в нестабильном состоянии до тех пор, пока тот не будет добавлен в display list и не пройдет все стадии инициализации (createChildren, commitProperties, measure, updateDisplayList и пр).
Если запустить тест до этого события, то в зависимости от скорости и загруженности компьютера, на котором запущены тесты, мы будем получать компонент в немного различных состояниях. Чтобы этого не произошло, мы должны дождаться события CREATION_COMPLETE.
Предположим, мы хотим создать кнопку, которая не бы только пряталась при установке visible=false, но и при этом не влияла на расположение других компонентов, то есть includeInLayout = false.
Тут конечно можно было includeInLayout = visible задать и в самой функции set visible, но хочется показать более общий случай, когда есть много взаимосвязанных свойств и все их нужно проапдейтить именно в commitProperties.
Чтобы «на глаз» проверять, что у нас все работает правильно, запустим тестовое приложеньице:
Теперь попробуем написать сам тест:
1. Как видно из кода, я использовал слушатель на внутреннее событие UIComponent 'includeInLayoutChanged' чтобы поймать изменения свойства includeInLayout. В документации по FlexUnit [1] рекомендуют использовать FlexEvent.VALUE_COMMIT, но оно не всегда диспатчится, приходится его диспатчить самому.
В самом общем случае, когда трудно выделить какое-либо событие, после которого можно проверять свойства, можно тестировать компонент как любой другой асинхронный процесс: установить таймер с гарантированно большим интервалом. Тогда наш тест перепишется так:
Если нужно делать много таких тестов с таймером, то инициализацию и обнуление таймера нужно вынести в соответствующие методы [Before] и [After].
2. Этот тест писался под Flex Sdk 3.*, если вы работаете с Flex 4.*, вместо Application.application.addChild(myBtn) нужно использовать FlexGlobals.topLevelApplication.addChild(myBtn).
3. Недостаток добавления тестируемых компонентов непосредственно в Application.application в том, что компонент виден в окне тест-раннера, а также в том, что стили тест-раннера могут повлиять на стили вашего компонента.
Чтобы избежать этого, в FlexUnit есть специальный класс UIImpersonator, который эмулирует добавление компонента в display list. К сожалению, мне не удалось заставить работать UIImpersonator в Ant-сборках и из Intellij — получаю ошибку «Timeout Occurred before expected event». Возможно кто-то подскажет почему. Пример как использовать UIImpersonator см. ниже в комментах.
UPD: Чтобы работало из Intellij и ant, нужно удалить из списка подключенных библиотек flexunit-4.1.0-33-as3_3.5.0.12683.swc. Она конфликтует с аналогичной для флекса.
1. Дока по FlexUnit: docs.flexunit.org/index.php?title=Main_Page
2. Создание и запуск юнит-тестов из FlashBuilder: habrahabr.ru/blogs/Flash_Platform/89487
3. Настольная книга по рефакторингу: Michael Feathers. Working Effectively with Legacy Code.
Стандартный подход к решению подобных задач — начать с написания юнит-тестов, покрывающих нынешнее поведение компонента, попутно проясняя для себя особенности его устройства.
Только после этого можно начинать пошаговый рефакторинг и расширение функциональности, постоянно прогоняя тесты на предмет не сломалось ли чего в результате изменений. [3]
Задача однако осложняется тем, что визуальные компоненты во флекс имеют многофазную асинхронную процедуру инициализации и обновления свойств, и для них требуется особые средства для написания тестов. FlexUnit 4 позволяет легко справляться с этой задачей, и ниже я покажу как это делать, а заодно и раскрою пару нюансов.
При тестировании наследников UIComponent мы должны учитывать, что большинство свойств компонента находятся в нестабильном состоянии до тех пор, пока тот не будет добавлен в display list и не пройдет все стадии инициализации (createChildren, commitProperties, measure, updateDisplayList и пр).
Если запустить тест до этого события, то в зависимости от скорости и загруженности компьютера, на котором запущены тесты, мы будем получать компонент в немного различных состояниях. Чтобы этого не произошло, мы должны дождаться события CREATION_COMPLETE.
Предположим, мы хотим создать кнопку, которая не бы только пряталась при установке visible=false, но и при этом не влияла на расположение других компонентов, то есть includeInLayout = false.
<?xml version="1.0"?>
<mx:Button xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
override public function set visible(value:Boolean):void {
super.visible = value;
invalidateProperties();
}
override protected function commitProperties():void {
super.commitProperties();
includeInLayout = visible;
// раскомментируйте это, если нужно слушать событие VALUE_COMMIT
//dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
]]></mx:Script>
</mx:Button>
Тут конечно можно было includeInLayout = visible задать и в самой функции set visible, но хочется показать более общий случай, когда есть много взаимосвязанных свойств и все их нужно проапдейтить именно в commitProperties.
Чтобы «на глаз» проверять, что у нас все работает правильно, запустим тестовое приложеньице:
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*">
<local:HidingButton id="hidingBtn"
label="I'll hide"/>
<mx:Button label="try me"
click="hidingBtn.visible = ! hidingBtn.visible"/>
</mx:Application>
Теперь попробуем написать сам тест:
package {
import flash.events.Event;
import flexunit.framework.Assert;
import mx.core.Application;
import mx.events.FlexEvent;
import org.flexunit.asserts.assertEquals;
import org.flexunit.async.Async;
public class HidingButtonAsyncTest {
public var myBtn:HidingButton;
// Этот код выполняется ПЕРЕД запуском каждого теста,
// помеченного метатегом [Test]
[Before( async )]
public function setUp():void {
myBtn = new HidingButton();
// Дождаться события CREATION_COMPLETE которое произойдет после вызова addChild
Async.proceedOnEvent(this, myBtn, FlexEvent.CREATION_COMPLETE, 1000);
Application.application.addChild(myBtn);
}
// Этот код выполняется ПОСЛЕ запуска каждого теста,
// помеченного метатегом [Test]
[After( async )]
public function tearDown():void {
Application.application.removeChild(myBtn);
myBtn = null;
}
[Test(async, description="тест значений по умолчанию")]
public function testDefaultState():void {
assertEquals(myBtn.visible, true);
assertEquals(myBtn.includeInLayout, true);
}
[Test(async, description="скрываем кнопку")]
public function testHideButton():void {
myBtn.addEventListener('includeInLayoutChanged',
Async.asyncHandler(this, handleVerifyProperty, 100, null, handleEventNeverOccurred), false, 0, true);
myBtn.visible = false;
// эта функция уникальная для каждого теста, вызывается при срабатывании нужного события
function handleVerifyProperty(event:Event, passThroughData:Object):void {
assertEquals(myBtn.includeInLayout, false);
}
}
// обработчик таймаута, общий для всех тестов
private function handleEventNeverOccurred(passThroughData:Object):void {
Assert.fail('Pending Event Never Occurred');
}
}
}
Нюансы
1. Как видно из кода, я использовал слушатель на внутреннее событие UIComponent 'includeInLayoutChanged' чтобы поймать изменения свойства includeInLayout. В документации по FlexUnit [1] рекомендуют использовать FlexEvent.VALUE_COMMIT, но оно не всегда диспатчится, приходится его диспатчить самому.
В самом общем случае, когда трудно выделить какое-либо событие, после которого можно проверять свойства, можно тестировать компонент как любой другой асинхронный процесс: установить таймер с гарантированно большим интервалом. Тогда наш тест перепишется так:
[Test(async, description="проверка свойства по таймауту")]
public function testHideButtonWithTimer():void {
var timer:Timer = new Timer(50,1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE,
Async.asyncHandler(this, handleVerifyProperty, 100, {}, handleEventNeverOccurred), false, 0, true);
myBtn.visible = false;
timer.start();
function handleVerifyProperty(event:Event, passThroughData:Object):void {
assertEquals(myBtn.includeInLayout, false);
}
}
Если нужно делать много таких тестов с таймером, то инициализацию и обнуление таймера нужно вынести в соответствующие методы [Before] и [After].
2. Этот тест писался под Flex Sdk 3.*, если вы работаете с Flex 4.*, вместо Application.application.addChild(myBtn) нужно использовать FlexGlobals.topLevelApplication.addChild(myBtn).
3. Недостаток добавления тестируемых компонентов непосредственно в Application.application в том, что компонент виден в окне тест-раннера, а также в том, что стили тест-раннера могут повлиять на стили вашего компонента.
Чтобы избежать этого, в FlexUnit есть специальный класс UIImpersonator, который эмулирует добавление компонента в display list.
UPD: Чтобы работало из Intellij и ant, нужно удалить из списка подключенных библиотек flexunit-4.1.0-33-as3_3.5.0.12683.swc. Она конфликтует с аналогичной для флекса.
Список литературы
1. Дока по FlexUnit: docs.flexunit.org/index.php?title=Main_Page
2. Создание и запуск юнит-тестов из FlashBuilder: habrahabr.ru/blogs/Flash_Platform/89487
3. Настольная книга по рефакторингу: Michael Feathers. Working Effectively with Legacy Code.