Prerequisite knowledge
Experience with ActionScript 3 and building Adobe AIR applications for iOS is required to make the most of this tutorial. Specifically, you will need to know how to create a new project, add classes, add iOS certificates and provisioning files, add native extensions, and build for iOS.
Required products
Flash Builder (Download trial)
Additional required other products
Download and learn more about the Beta Testing Adobe AIR native extension
User level


Prerequisite knowledge

Experience with ActionScript 3 and building Adobe AIR applications for iOS is required to make the most of this tutorial. Specifically, you will need to know how to create a new project, add classes, add iOS certificates and provisioning files, add native extensions, and build for iOS.


Additional required other products

Download and learn more about the Beta Testing Adobe AIR native extension

User level


Testing apps and games is a vital activity that doesn’t always receive the priority it deserves. The Beta Testing Adobe AIR native extension (available as part of the Adobe Gaming SDK) and the TestFlight service help simplify testing of your AIR apps and games. This article covers how to set up an account with TestFlight and configure it to fit your needs. You’ll learn how to use the Beta Testing native extension with ActionScript 3 to log messages to TestFlight and monitor crashes and errors coming from your application.

What is TestFlight?

TestFlight is a free service for (beta) testing your application. It provides a simple and uniform way to distribute builds—in the form of IPA (iOS) and APK (Android) files—to testers and team members.
TestFlight also has an SDK that you can use to log messages, solicit tester feedback, and see crash reports and errors. You can view all of this information in one place.
You can also set up TestFlight distribution lists. If your company has different locations, for example, you can have a different distribution list for each location. You can set up different list for external users and internal testers. Alternatively, if you want different people to test different features, you can make lists specific to those features.

Setting up a TestFlight account

The first step is to visit and sign up for an account. Be sure to set the Developer switch to On (see Figure 1). This enables you to upload and distribute your own builds.
Enabling the Developer option.
Figure 1: Enabling the Developer option.
After you sign up you’ll receive an email with further instructions. Follow those instructions and you will be automatically logged in and presented with a dashboard (see Figure 2).
TestFlight login.
Figure 2: TestFlight login.

Configuring TestFlight

The next step is to create a team.
  1. On the dashboard, click Create A New Team (see Figure 3).
Creating a new team.
Figure 3: Creating a new team.
  1. Type a name for the new team. This can be the name of an application, client, or project. I prefer to use client names. For test purposes I typed Adobe Developer Connection (see Figure 4).
  2. Click Save.
Specifying a team name.
Figure 4: Specifying a team name.

Adding an application

There are two ways of adding an application with TestFlight. The first is to directly upload a build and have TestFlight fill in the blanks for you. The second is to provide the necessary information yourself.
Follow these steps to use the second method:
  1. Click Apps in TestFlight and then click Create An App.
  2. Type in a name and a BundleID for your app (see Figure 5).
  3. Select iOS as the platform.
  4. Click Save.
For testing purposes I named my app ADC Test App and typed com.adc.test.ADCTest as the BundleID. I’ll use this BundleID  later in the actual app as the class name. This is also the name of the id node in the descriptor.xml file.
Adding an app in TestFlight.
Figure 5: Adding an app in TestFlight.
Once you click ‘Save’ you will be presented with your application token (see Figure 6). Your app will need this to communicate with TestFlight.
The application token.
Figure 6: The application token.

Inviting team members and users to test

You create a team for your project by inviting people to it from the dashboard (see Figure 7).
Inviting new teammates and testers from the dashboard.
Figure 7: Inviting new teammates and testers from the dashboard.
You can invite people by sending them an email or by obtaining a link to your TestFlight project and sharing it with them. The first option is handy if you want to invite people separately with a personal message. The second option is ideal when recruiting people via social media.
The link for my project is Sign up and add yourself to the project so you can play around with TestFlight and get a feel for how it works from a tester’s perspective. (I won’t actually distribute the test app I created for this tutorial because that would also mean I would need to add your UDID to my provisioning profile. Apple limits how many users a provisioning profile can contain, so this isn’t feasible. You can, however, see the download from within your profile when viewing this link on your iOS device.)
  1. In the top menu in TestFlight click People.
  2. From this screen you can manage the members of your team. You can organize team members by adding people to a distribution list.
  3. To get started click Add Distribution List.
  4. I frequently work with external testers so I always create two lists to start with: External and Internal.
  5. Use the interface to create a distribution list and add yourself to it.
Now you have set up TestFlight, and it is time to write some code to make use of it.

Writing the code and using the Beta Testing native extension

To help you better understand how TestFlight and the Beta Testing native extension work, the steps below walk through the creation of a small application with buttons that trigger specific TestFlight methods.
  1. Open Flash Builder and create a new ActionScript Mobile Project. Add the correct certificate and provisioning files.
  2. Copy the betatesting.ane file to your library folder.
  3. Right-click your project in Package Explorer and select Properties.
  4. Select ActionScript Build Path and click the Native Extensions tab.
  5. Click Add ANE and navigate to the betatesting.ane file.
  6. Open the app’s descriptor.xml file (usually MyProject-app.xml) and locate the <id> node.
  7. Set the id to the BundleID you used earlier: com.adc.test.ADCTest.
  8. In the main class (mine is called listen for the Event.ACTIVATE event on  nativeApplication.
  9. The event handler should remove itself and load the UI, which is just a Sprite with a couple of buttons.
public function ADCTest() { /* * Make sure everything aligns correctly */ stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; NativeApplication.nativeApplication.addEventListener( Event.ACTIVATE, handleActivateApp ); } private function handleActivateApp( event : Event ) : void { NativeApplication.nativeApplication.removeEventListener( Event.ACTIVATE, handleActivateApp ); loadUI(); } private function loadUI() : void { var screen : ButtonScreen = new ButtonScreen(); addChild( screen ); }
  1. Add a ButtonScreen class that creates five buttons that invoke methods on the TestFlight API.
package com.adc.test.ui.screen { import com.adc.test.enum.CheckPoints; import com.adc.test.ui.buttons.ADCButton; import com.adobe.ane.testFlight.TestFlight; import flash.display.Sprite; import; import; /** * @author Sidney de Koning - Mannetje de Koning { } */ public class ButtonScreen extends Sprite { private var _tf : TestFlight; public function ButtonScreen() { addEventListener( Event.ADDED_TO_STAGE, build ); } private function build( event : Event ) : void { var i : int = 0; var offset : int = 100; var btn : ADCButton = new ADCButton( "Send log message" ); btn.addEventListener( MouseEvent.CLICK, handleSendLog ); btn.x = 0 + (stage.fullScreenWidth - btn.width) * 0.5; btn.y = btn.y + btn.height + (i++ * offset); addChild( btn ); btn = new ADCButton( "Open feedback view" ); btn.addEventListener( MouseEvent.CLICK, handleOpenFeedBackView ); btn.x = 0 + (stage.fullScreenWidth - btn.width) * 0.5; btn.y = btn.y + btn.height + (i++ * offset); addChild( btn ); btn = new ADCButton( "Pass checkpoint" ); btn.addEventListener( MouseEvent.CLICK, handlePassCheckPoint ); btn.x = 0 + (stage.fullScreenWidth - btn.width) * 0.5; btn.y = btn.y + btn.height + (i++ * offset); addChild( btn ); btn = new ADCButton( "Submit custom feedback" ); btn.addEventListener( MouseEvent.CLICK, handleSubmitCustomFeedBack ); btn.x = 0 + (stage.fullScreenWidth - btn.width) * 0.5; btn.y = btn.y + btn.height + (i++ * offset); addChild( btn ); if (TestFlight.isSupported) { _tf = new TestFlight( "YOUR_APP_TOKEN", true ); var stderr : Object = new Object(); stderr.key = "logToSTDERR"; stderr.value = "YES"; var console : Object = new Object(); console.key = "logToConsole"; console.value = "YES"; var optArr : Array = new Array(); optArr[0] = stderr; optArr[1] = console; _tf.setOptions( optArr ); _tf.passCheckPoint( CheckPoints.SOME_SECTION_CP ); } } private function handleOpenFeedBackView( event : MouseEvent ) : void { if (TestFlight.isSupported) { _tf.passCheckPoint( CheckPoints.SOME_SECTION_BUTTON_FEEDBACK_CP ); _tf.openFeedBackView(); } } private function handlePassCheckPoint( event : MouseEvent ) : void { if (TestFlight.isSupported) { _tf.passCheckPoint( CheckPoints.SOME_SECTION_BUTTON_X_CP ); } } private function handleSubmitCustomFeedBack( event : MouseEvent ) : void { if (TestFlight.isSupported) { _tf.passCheckPoint( CheckPoints.SOME_SECTION_BUTTON_Y_CP ); _tf.submitCustomFeedBack( "Here is my custom feedback for the app, this shoudl come from custom UI / textfield" ); } } private function handleSendLog( event : MouseEvent ) : void { if (TestFlight.isSupported) { _tf.log( "Sending log message to TestFlight" ); } } } }
  1. Add a CheckPoints class to hold the messages. This is just a file with public constants containing strings.
package com.adc.test.enum { /** * @author Sidney de Koning - Mannetje de Koning { } */ public class CheckPoints { public static const SOME_SECTION_CP : String = "Some Section"; public static const SOME_SECTION_BUTTON_X_CP : String = "Some Section Button X"; public static const SOME_SECTION_BUTTON_Y_CP : String = "Some Section Button Y"; public static const SOME_SECTION_BUTTON_FEEDBACK_CP : String = "Some Section Button Feedback"; } }
As you can see, every time the app calls a method on the TestFlight component it checks to see if TestFlight is supported. This ensures it is only used on supported platforms. Even though TestFlight does support Android, the current version of the AIR native extension does not.  
Here is what happens under the hood. When you pass the app token to the constructor it will call the native takeoff() method. The component will store its session data and submit it to TestFlight so you can see it in the TestFlight dashboard. The constructor also takes a second parameter named setDeviceIdentifier. When this is set to true, TestFlight ties your Universal Device Identifier (UDID) to the current session. But this can only be used while debugging or in the beta test phase; it is not meant for production apps.
As of May 1, Apple began rejecting apps that make use of this UDID because many advertisers were abusing it. Apple has come up with an alternative in the form of a Vendor and Advertising identifier. It’s a number that is unique but not tied to a specific device and can also be reset. For more on TestFlight’s response to this decision, see TestFlight SDK UDID Access.
To summarize: when you submit your app to the App Store set this parameter to false.

TestFlight methods

The Beta Testing native extension provides methods for communicating with the TestFlight API. At the time of this writing Questions() and some other methods are yet to be implemented.
When testing applications it is useful to know if the user has passed a specific point in the app, tapped a specific button, or reached a certain screen within a game. Such checkpoints are the cornerstone of TestFlight. As a developer, you can specify these checkpoints at crucial places within your app by calling the passCheckPoint method. You only need to provide the method with a name of the checkpoint. As you can see in the example these strings are defined in the CheckPoints class, which provides a centralized place for them.
One thing I really like about TestFlight is the ability to ask users for feedback. If you are testing an app and you find an issue or a bug, you typically have to send an email to report it or log the issue in an issue tracker. This can really interrupt your workflow.
Calling openFeedBackView() opens an overlay with a single feedback field. When a user taps Submit on this overlay their feedback is submitted to your app’s profile in TestFlight. You can see that the feedback comes "Via SDK" (see Figure 8).
Feedback submitted via openFeedBackView().
Figure 8: Feedback submitted via openFeedBackView().
If you prefer to set the feedback using your own overlay/form, use submitCustomFeedBack.
This method takes a string with the actual feedback, which can, for example, come from a text field. So you can skin the UI to fit your app. Like feedback submitted via  openFeedBackView, you can see the feedback in TestFlight labeled as "via SDK custom" (see Figure 9).
Feedback submitted via submitCustomFeedBack().
Figure 9: Feedback submitted via submitCustomFeedBack().
The setOptions() method lets you choose the manner of logging messages. The TestFlight component includes three different loggers:
  • TestFlight logger
  • Apple System Log logger (ASL)
  • STDERR logger
If you wanted to only log to the console and not to STDERR you would write;
var stderr : Object = new Object(); stderr.key = "logToSTDERR"; stderr.value = "NO"; var console : Object = new Object(); console.key = "logToConsole"; console.value = "YES"; var optArr : Array = new Array(); optArr[0] = stderr; optArr[1] = console;
You would then pass this to the setOptions() method before the actual logging.
_tf.setOptions( optArr );
The TestFlight documentation provides more details:
"Each of the loggers log asynchronously and all TFLog calls are non blocking. The TestFlight logger writes its data to a file which is then sent to our servers on Session End events. The Apple System Logger sends its messages to the Apple System Log and are viewable using the Organizer in Xcode when the device is attached to your computer. The ASL logger can be disabled by turning it off in your TestFlight options
The STDERR logger sends log messages to STDERR so that you can see your log statements while debugging. The STDERR logger is only active when a debugger is attached to your application. If you do not wish to use the STDERR logger you can disable it by turning it off in your TestFlight options"
Objective-C uses YES for Booleans that are true and NO for Booleans that are false. By default the log options are set to true  ( YES ), so by default all loggers are enabled.
log( message:String )
When debugging on a device, log messages can be written to the log by calling log(). The setOptions() method determines where the log message goes. Currently these messages are only used locally; they do not show up in TestFlight. The TFLog calls are sent to TestFlight (see Figure 10).
Received log messages
Figure 10. Received log messages

Uploading apps to TestFlight

Now you are ready to build the example app, create an IPA file, and distribute it. This distribution can be done in different ways. TestFlight provides a standard way of uploading your app via a web form as well as  a desktop uploader.
Desktop Uploader
To download TestFlight's Desktop App visit Once it is installed, simply drag-and-drop your freshly created .IPA file to the designated hotspot (see Figure 11). Then, fill in your release notes for the version and select the people or list to which you want to distribute. TestFlight will distribute your build to your testers.
The TestFlight Desktop App
Figure 11: The TestFlight Desktop App.
API Uploading
TestFlight also provides a curl API for uploading apps. See for more details. You can use this API in an automated build or within your favorite IDE.
For example, I used this API in an Ant build file, which you can find in the in the ant folder of the sample files. To use this script, simply update the properties in the file to fit your project. This script automatically uploads the build to TestFlight.
<project name="TestFlight Upload" basedir="."> <property file=""/> <target name="upload"> <exec executable="curl"> <arg value="" /> <arg value="-F file=@${ipa.file}" /> <arg value="-F api_token='${api.token}'" /> <arg value="-F team_token='${team.token}'" /> <arg value="-F notes='${release.notes}'" /> <arg value="-F notify=True" /> <arg value="-F replace=False" /> <arg value="-F distribution_lists='Internal'" /> </exec> </target> </project>
Here is my file:
#Build properties for upload to TestFlight release.notes=This build was uploaded via the upload API${}-debug ipa.file=../export/${}.ipa dSYM.file=../export/${}.app.dSYM api.token=REPLACE_WITH_YOUR_API_TOKEN team.token=REPLACE_WITH_YOUR_TEAM_TOKEN

Where to go from here

You’ve seen the methods available when using the Beta Testing native extension, you created a test app to tie it all together, and you uploaded your build to TestFlight. What else is there? Just a little bit more.
In the beta test phase (and at other times) it is useful to have an uncaught error handler in your app. You can create a specific checkpoint for such errors that sends the error message as custom feedback to TestFlight. You can even include the getStackTrace() results of the error to help developers understand why and how the app crashed.
This article is by no means a definitive guide to TestFlight, so I encourage you to continue exploring and try it out on your own iOS app.