По работе как-то потребовалось добавить функциональность в один самописный 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.
