Как стать автором
Обновить

Особенности тестирования Flex UI-компонентов с помощью FlexUnit 4

Время на прочтение5 мин
Количество просмотров2.5K
По работе как-то потребовалось добавить функциональность в один самописный flex-компонент. При этом важно было не поломать уже существующее поведение, т.к. компонент за время своего существования был использован в нескольких приложениях и оброс наследниками.

Стандартный подход к решению подобных задач — начать с написания юнит-тестов, покрывающих нынешнее поведение компонента, попутно проясняя для себя особенности его устройства.
Только после этого можно начинать пошаговый рефакторинг и расширение функциональности, постоянно прогоняя тесты на предмет не сломалось ли чего в результате изменений. [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. К сожалению, мне не удалось заставить работать 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.
Теги:
Хабы:
+11
Комментарии8

Публикации