Thursday, August 11, 2011

Flex 3 Test Automation using QTP

This information has been sitting in my “to publish” list for a while now, so I am finally going to put it out there since a couple people have been asking about it. It describes my experiences with automating user interface interaction testing using QTP, and all the things I had to workaround.

1. Setting up and running QTP

1.1 Add “automation_charts.swf” to the Flex SDK directory

Example: C:\Program Files\Adobe\Flex Builder 3 Plug-in\sdks\3.2.0\frameworks\libs

1.2 Add the QTP libraries to your project compiler options

Add the following to the compiler options on the project:

-include-libraries "C:\Program Files\Adobe\Flex Builder 3 Plug-in\sdks\3.2.0\frameworks\libs\automation.swc" "C:\Program Files\Adobe\Flex Builder 3 Plug-in\sdks\3.2.0\frameworks\libs\automation_agent.swc" "C:\Program Files\Adobe\Flex Builder 3 Plug-in\sdks\3.2.0\frameworks\libs\automation_charts.swc" "C:\Program Files\Adobe\Flex Builder 3 Plug-in\sdks\3.2.0\frameworks\libs\qtp.swc"

The absolute path must be used, relative will not work.

This will force the libraries into your main application SWF, which should increase its size by about 1.2 MB

1.3 Run your Flex application on a web server.

You must run the application on a web server, it cannot run locally. If you attempt to run it locally you will get lots of browser scripting errors.

Example: http://myserver/foo/bar.html

1.4 You must be running Flash 9

The computer running the application and QTP must have Flash 9, otherwise system popups will be blocked by Flash Player security

1.5 You must run QTP and the browser window in the same monitor.

The computer running the application and QTP must be doing so in the same monitor window in the event that two monitors are being used, more on this later.

 

2. Required Code Changes

For various reasons when doing any type of user interface testing in Flex you have to make code changes.

2.1 Create new build and deployment

You will need to create a new variation of your build that compiles the automation libraries into the application binary (and all modules). This is because you don’t want to deploy your release binaries with the QTP automation classes compiled into them. This is because it significantly increases the size of your SWF.

2.2 Recursively set the automation hierarchy when running for automation

In order for the QTP to be able to playback button presses to objects deep within the component hierarchy, an automation value has to be set throughout the application at runtime, see “QTP Playback is slow” for information as to why and how.

2.3 Creation of automation delegates for every custom components that QTP does not recognized.

QTP will not recognized interactions with items that inherit directly from UIComponent directly, or are otherwise not a standard button, or text component, or control. See “Custom Automation Delegates” for more information.

 

3. QTP Specifics and Problems

3.1 Creating a Custom Flex Automation Delegate

References

Summary

Delegates exist so that you do not have to place automation related code in you standard components, after all, most people don’t want to run their applications with automation support. Delegates are provided for all the framework classes and will generally work out of the box for any framework class you extend. The need to write custom delegates arises if you create a custom component that directly extends UIComponent or if you have complex requirements for a component which already has a framework provided delegate.

A delegate has to be created for each custom class. Example:

package

{

import flash.display.DisplayObject;

import mx.core.IInvalidating;

import randomWalkClasses.RandomWalkEvent;

import mx.automation.IAutomationObject;

import mx.automation.AutomationIDPart;

import mx.automation.delegates.core.UIComponentAutomationImpl;

import mx.automation.IAutomationObjectHelper;

import mx.automation.Automation;

import mx.automation.events.AutomationRecordEvent;

import flash.events.Event;

[Mixin]

public class RandomWalkDelegate extends UIComponentAutomationImpl

{

       private var walker:RandomWalk

       public function RandomWalkDelegate(randomWalk:RandomWalk)

       {

               super(randomWalk);

               randomWalk.addEventListener(RandomWalkEvent.ITEM_CLICK, itemClickHandler);

               randomWalk.addEventListener(AutomationRecordEvent.RECORD, labelRecordHandler);

               walker = randomWalk;

       }

       public static function init(obj:DisplayObject):void

       {

              Automation.registerDelegateClass(RandomWalk, RandomWalkDelegate);

       }

       private function itemClickHandler(event:RandomWalkEvent):void

       {

               recordAutomatableEvent(event);

       }

       public function labelRecordHandler(event:AutomationRecordEvent):void

       {

               // if the event is not from the owning component reject it.    

               if(event.replayableEvent.target != uiComponent)

               //event.preventDefault(); can also be used.

              event.stopImmediatePropagation();

       }

       override public function get numAutomationChildren():int

       {

              var numChildren:int = 0;

              var renderers:Array = walker.getItemRenderers();

              for(var i:int = 0;i< renderers.length;i++)

              {

                numChildren += renderers[i].length;

              }

              return numChildren;

       }

       override public function getAutomationChildAt(index:int):IAutomationObject

       {

              var numChildren:int = 0;

              var renderers:Array = walker.getItemRenderers();

              for(var i:int = 0;i < renderers.length;i++)

              {

                  if(index >= numChildren)

                  {

                          if(i+1 < renderers.length && (numChildren + renderers[i].length) <= index)

                          {

                             numChildren += renderers[i].length;

                             continue;

                          }

                          var subIndex:int = index - numChildren;

                          var instances:Array = renderers[i];          

                          return (instances[subIndex] as IAutomationObject);

                   }

              }

       return null;

       }

       override public function createAutomationIDPart(child:IAutomationObject):Object

       {

               var help:IAutomationObjectHelper = Automation.automationObjectHelper;

               return help.helpCreateIDPart(this, child);  

       }  

       override public function resolveAutomationIDPart(part:Object):Array   

       {            

               var help:IAutomationObjectHelper = Automation.automationObjectHelper;

               return help.helpResolveIDPart(this, part as AutomationIDPart);  

       }

       override public function replayAutomatableEvent(event:Event):Boolean

       {

               var help:IAutomationObjectHelper = Automation.automationObjectHelper;

               if (event is RandomWalkEvent)

               {

                 var rEvent:RandomWalkEvent = event as RandomWalkEvent

                         help.replayClick(rEvent.itemRenderer);

                        (uiComponent as IInvalidating).validateNow();

                        return true;

               }

               else

               return super.replayAutomatableEvent(event);

       }

}

}

The TEAFlexCustom.xml for Mercury on all machines that intend to do automation has to be updated to include details about the custom class. Example:

<TypeInformation xsi:noNamespaceSchemaLocation="ClassesDefintions.xsd"

Priority="0" PackageName="TEA" Load="true" id="Flex" xmlns:xsi="http:/

/www.w3.org/2001/XMLSchema-instance">

       <ClassInfo Name="FlexRandomWalk" GenericTypeID="randomwalk" Extends="FlexObject" SupportsTabularData="false">

              <Description>FlexRandomWalk</Description>

              <Implementation Class="RandomWalk"/>

              <TypeInfo>

                      <Operation Name="Select" PropertyType="Method" ExposureLevel="CommonUsed">

                             <Implementation Class="randomWalkClasses::RandomWalkEvent" Type="itemClick"/>

                             <Argument Name="itemRenderer" IsMandatory="true" >

                                    <Type VariantType="String" Codec="automationObject"/>

                                    <Description>User clicked item</Description>

                             </Argument>

                      </Operation>

              </TypeInfo>

              <Properties>

                      <Property Name="automationClassName" ForDescription="true">

                             <Type VariantType="String"/>

                             <Description>To be written.</Description>

                      </Property>

                      <Property Name="automationName" ForDescription="true">

                             <Type VariantType="String"/>

                             <Description>The name used by the automation system to identify an object.</Description>

                      </Property>

                      <Property Name="className" ForDescription="true">

                             <Type VariantType="String"/>

                             <Description>To be written.</Description>

                      </Property>

                      <Property Name="id" ForDescription="true" ForVerification="true">

                             <Type VariantType="String"/>

                             <Description>Developer-assigned ID.</Description>

                      </Property>

                      <Property Name="automationIndex" ForDescription="true">

                             <Type VariantType="String"/>

                             <Description>The object's index relative to its parent.</Description>

                      </Property>

                      <Property Name="openChildrenCount" ForVerification="true" ForDefaultVerification="true">

                             <Type VariantType="Integer"/>

                             <Description>Number of children open currently</Description>

                      </Property>

              </Properties>

       </ClassInfo>

</TypeInformation>

3.2 File Selection fails to playback

When playing back the selection of a file in a System32 file dialog, the following error is thrown in QTP with the message “Object not visible”:

image041

Cause: Dual Monitors
Reference: http://forums11.itrc.hp.com/service/forums/questionanswer.do?threadId=1200371

Summary: This error is sometimes thrown when the browser, the secondary window (the file dialog), and QTP are not all in the same monitor.

Solution: Display all the windows in the same monitor.

3.3 QTP Playback is slow

Cause #1: Bug with automation in Flex 3 SDK

Summary: QTP playback is slower because of the implementation of automation libraries for Flex in the version 3 SDK. The issue does not exist in the version 2 SDK and has been resolved in the 4 SDK.

This is specifically because deep container hierarchies are slow to iterate through.

In the Flex 3 SDK automation scripts are significantly slower in deep component and container hierarchies. This is because the scripts have to iterate through the entire hierarchy in order to find the component that is being used. This issue was resolved in the Flex 4 SDK,

For example pressing the upload button to open the file dialog took 4 minutes, because the button is so deep within the hierarchy.

Potential Workaround: Set the automationName of the container and component

Workaround Summary

The automationName is a property available every component which designates the name that is used to identify a component in test automation. If no automationName is specified in Flex containers use their id attribute, and if no id attribute is present the container will have an automationName generated for it prefixed with “index”. Buttons however use the value of their label as their automationName when it is not specified.

It was theorized that setting the automationName may allow quicker access to the component, without having to iterate through the entire component container hierarchy, but this did not make a difference.

For example a button without an automationName would be accessed in QTP like the following:

Browser(#).FlexApplication(“Client”).FlexButton(“Upload and Print”).Click

Where “Upload and Print” is the generated automationName from the button label, but if automationName were set on the component in flex to “myButton” then the button would be accessed like the following:

Browser(#).FlexApplication(“Client”).FlexButton(“myButton”).Click

Actual Workaround: Set every container in the application hierarchy to showAutomationInHierarchy to true

References:

Workaround Summary

By default when QTP has to access a Flex component, it has to access that component by automationName. That component is located within an application by iterating starting with the application container and all of its children until the component within that automationName is found. The result is that a component that is deep within a container hierarchy is slow to find. It is for this reason and others, that it is recommend that Flex applications do not overuse container components. Unfortunately in the case of a large already built application, it is too late to go back and redesign the component container structure.

The solution is to specify every automationName within a hierarchy in order to limit the looping needed to find a component. This can be done by setting the showAutomtationInHierarchy value on a container to true, which will cause it to be detected in QTP even if that component is not directly involved in the interaction. If a Flex application uses modules to incrementally load itself over time, it is not possible on startup to recursively set the hierarchy value. Instead after a module is loaded, the module and the components within can recursively have this value set.

public function configureAutomation(obj:DisplayObject) : void

{

        if (obj is Container)

        {

            var c:Container = obj as Container;

                c.showInAutomationHierarchy = true;

                var children:Array = c.getChildren();

            for(var i:int=0; i < children.length; i++)

                {

                    var child:DisplayObject = c.getChildAt(i);

                    configureAutomation(child);

            }

        }

}

For example without this change the Upload button in the Client is accessed using QTP like the following, which requires 4 minutes to process:

Browser("#").FlexApplication("Client").FlexDividedBox("workflowUploadBox").FlexButton("uploadFirstBtn").Click

When all of the containers have the automation hierarchy set to show, that same button is accessed like the following in QTP and happens instantly:

Browser("#").FlexApplication("Client").FlexContainer("index:29").FlexCanvas("_DocumentViewModule_DocumentVi").FlexDividedBox("workflowUploadBox").FlexBox("index:0").FlexCanvas("newDocView").FlexBox("_NewDocument_VBox1").FlexCanvas("uploadView").FlexBox("_UploadView_HBox2").FlexButton("uploadFirstBtn").Click

Cause #2: QTP 9.1 is slow

Summary: Script playback in QTP 9.1 is very slow, and switching to 9.5 makes the playback significantly faster.

 

4. Maintainability Concerns

This is a concern with Flex and HTML based user interface drivers, in which the actor has to know the ID of the component in order to play back some action on it. In languages like JSF and Flex you don’t have to give an XML component an ID, which results in that component getting a dynamically generated ID.

image001

In Flex the problem is that when a component is not explicitly named, that name is calculated at compile time. The problem with the generated name is that it is based on a component’s position in its hierarchy. For example when a new Canvas is added, the names of the other two canvases change as shown by the highlighting in red.

It should also be noted that buttons are labeled by their text, which means of the text on the button changes the script will also have to change.

In order to ensure that a component’s name to QTP is always the same, the developer has to set the component’s automation name in the code for buttons and labels, and the component’s ID for boxes and canvases.

No comments: