Monthly Archives: August 2009

Continuous Integration with Flex, Hudson, and ArcGIS Server, Part 3

(Part 1Part 2Part 4)

One of the tenets of continuous integration is automated testing.  The most basic form of testing, in this context, is unit testing, where each class or method is tested without running the other bits of the application.  Unit testing is focused and, hopefully, simple.  In this post, I am going to add unit testing to our application and get ant to run the tests.  There are several unit testing frameworks out there, and we’re going to use Fluint. Fluint and FlexUnit have kinda merged into FlexUnit4, which is in alpha.  FlexUnit4 is, howyousay, super-fantastic, but it doesn’t currently offer any ant tasks.  When it does, I’ll be moving to it post haste.

Go get Fluint from here.  Put the fluint-1.1.1.swc file in the libs folder of your project.  We are now ready to start with some simple Test Driven Development.  Let’s add a button that zooms the map one level in, shall we?  As all examples, it’s contrived, but the point is to focus on the process.   The process here will follow simple TDD: write a test, watch it fail, write the code to make it pass.  In order to be able to watch the test fail, we’re need a bit of a foundation.  For the sake of this article, I have made a “tests” folder in the src directory where I will store the test bits.  Also, we will need a test runner for when we want to manually run our test suite.  That can be downloaded from here and copied to the src directory as well.  Once the runner (FlexTestRunner.mxml) is in the src dir, be sure to go into the project properties and add it to the list of Flex Applications for the project.  if you’ve done all that properly, then you will be able to run the FlexTestRunner application, which runs the Fluint test suites, by default.

image

We don’t want to run the framework tests, so remove all mentions of FrameworkSuite from the code in FlexTestRunner.mxml.  In Fluint, a test runner runs test suites and test suites are composed of test cases.  We will make our own test suite, called ZoomButtonTestSuite, and our own test case called (you guessed it) ZoomButtonTestCase.  These live in the aforementioned “tests” dir.

The test suite is simple:

package tests
{
    import net.digitalprimates.fluint.tests.TestSuite;
    public class ZoomButtonTestSuite extends TestSuite
    {
        public function ZoomButtonTestSuite()
        {
            addTestCase(new ZoomButtonTestCase());
        }

    }
}

All we do is pull in our test case, which looks like:

package tests
{
    import net.digitalprimates.fluint.tests.TestCase;

    public class ZoomButtonTestCase extends TestCase
    {
        public function ZoomButtonTestCase()
        {
            super();
        }
    }
}

Before we go on, go back to FlexTestRunner.mxml and add ZoomButtonTestSuite to the suiteArray in startTestProcess, so it looks like:

protected function startTestProcess( event:Event ) : void
{
    var suiteArray:Array = new Array();
    suiteArray.push( new ZoomButtonTestSuite() );

    testRunner.startTests( suiteArray );
}

We should be ready to right our first test.  For the ZoomButton, here are the specs:

  • When the user clicks the button, zoom the map in one level.

Pretty complicated, eh?  Here’s the test:

public function testZoomIn():void{
    var button:ZoomButton = new ZoomButton();
    var beforeLevel:uint= button.map.level;
    button.doZoom();

    assertTrue(button.map.level < beforeLevel);
}

I have started with the button API design, which is the real aim of our test.  Just from this test, you can see that:

  • The button is it’s own class.
  • The button has a property called map.
  • The button has a method called doZoom() (it has to be public for this example…not sure i like that)
  • The doZoom() method changes the map level.

This won’t even compile, so let’s get that happening.

package widgits
{
    import com.esri.ags.Map;
    import mx.controls.Button;

    public class ZoomButton extends Button
    {
        public var map:Map;
        public function ZoomButton()
        {
            super();
        }
        public function doZoom():void{

        }
    }
}

OK.  The ZoomButton extends the Button class, has a map property (setter/getter not used for brevity) and a doZoom() method.  Launch the test runner, and you’ll see:

image

About what we’d expect.  The button.map property is null.  However, I don’t want to create a full blown ESRI map for my tests.  It will be clunky and slow and difficult.  We need to fake the map.  This is where Mocks come in.

Mocking What We Don’t (need to) Understand

Just like unit testing frameworks, there are quite a few mocking frameworks in Flex land.  For this article, I am going to use MockAS3. (You have to build from source.  Or you can leave a comment with an e-mail and I’ll send you my copy).  Mock AS3 is nice, as it lets us mock a class and not just an interface.  ESRI does not implement an interface with its controls (like Map) for reasons that I don’t know, but I wish they would.  Also, this unit test is an interaction-based test, meaning we are going to set expectation of how we expect the ZoomButton and the map to interact, then verify those expectations as part of the test.  This is what mocking is, testing interactions and expectations presuming all dependent objects do what they are supposed to do.

So, what do we expect the ZoomButton to do in the doZoom() method?  We expect it to call map.zoomIn(), right?  Our first, naive test was testing properties on the map.  While this is testing a valid post-condition, it bleeds too far into testing the ESRI Map control and not the ZoomButton.  After all, the ZoomButton does not set the map level, it just calls a method on the map. If it does that, we are happy with our ZoomButton.  Any issues outside of it are not related to the ZoomButton and its purpose.  With this in mind, our new test looks like:

public function testZoomIn():void{
        var button:ZoomButton = new ZoomButton();
        var mapMock:MapMock = new MapMock();
        mapMock.mock.method('zoomIn').once;

        button.doZoom();

        mapMock.mock.verify();
}

The new test now creates a mock object for the ZoomButton.map property and tells it to expect the “zoomIn” method to be called one time.  We then test the doZoom method, followed by asking the mock to verify that our expectation was met.  Here is the mock:

package tests.mocks
{
    import com.anywebcam.mock.Mock;
    import com.esri.ags.Map;
    public class MapMock extends Map
    {
        public var mock:Mock;
        override public function MapMock()
        {
            mock = new Mock(this);
        }

        override public function zoomIn():void{
            mock.zoomIn();
        }

    }
}

This follows the pretty-good documentation on the Mock AS3 Google Code site for mocking classes.  We only need to mock the methods on which we are setting expectations.  Build and launch the test runner again.

image

Test still fails, but we get a new error:  Verifying Mock Failed. We haven’t coded our doZoom() method yet.

public function doZoom():void{
    if (map)
        map.zoomIn();
    else
        throw new Error("Map is not set");
}

Now, running the tests gives us sweet green….

image

Test Ant, Test Ant…

Now, we need to get these tests running with ant.  Luckily, Fluint ships with ant tasks (which are not available for FlexUnit4 at the time of this writing, but they assure me they are coming)  However, the ant tasks will not use the FlexTestRunner.mxml.  They use a different test runner with is Adobe AIR based, called (you guessed it) AirTestRunner, which you can download from here.  Double clicking on the .air files will launch the AIR installer.  Just follow all the prompts and it will install the FlexAirTestRunner to C:\Program Files\FluintAIRTestRunner.  In that directory you’ll see a .EXE file as well as a .SWF file, which is the actual test runner.  Another caveat to running the Fluint tests with ant is that the AIR test runner requires the use of Flex Modules.  What this means is that you have to create one or more Flex Modules for your tests, depending on how you want to structure the tests.  Basically, each module acts like a container for TestSuites that you want to run, then add the module to the ant task.  Enough talk.   A sample test module can be downloaded from the Fluint Google Code site, here or can be easily written.  A Fluint test module implements the ITestModule interface, which has a single method: getTestSuites().  Here is ours:

<?xml version="1.0" encoding="utf-8"?>
<mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" implements="net.digitalprimates.fluint.modules.ITestSuiteModule">
    <mx:Script>
        <![CDATA[

            public function getTestSuites() : Array
            {
                 var suiteArray : Array = new Array();
                suiteArray.push( new ZoomButtonTestSuite());
                return suiteArray;
            }
        ]]>
    </mx:Script>
</mx:Module>

Looks a lot like the FlexTestRunner.mxml, huh?  The only thing we have left to do now is add a test target to our ant script.  As some of you might have guessed, we need to add the Fluint ant tasks to our build.xml file.  The easiest (read: not necessarily RIGHT) way to do this is copy them into the c:\ant\lib directory.  You can get the JAR file with the ant tasks here.   Once they are in the ant lib directory, add the following line to your build.xml (right under the flexTasks is a good spot)

<taskdef name='fluint' classname='net.digitalprimates.ant.tasks.fluint.Fluint'  />

There, now we can use our Fluint tasks in the ant file.  Since the tests are using Flex modules, we’ll want to have a couple of targets in the build script: one that builds the module, and one that executes the test.  Heres’ the target that builds the module:

<target name="build-test-module">
     <echo>Building test module</echo>
    <mxmlc file='${srcpath.dir}/tests/ZoomButtonTestModule.mxml'  target-player="10.0.0"
        output='${deploypath.dir}/tests/ZoomButtonTestModule.swf' >
         <load-config filename='${FLEX_HOME}/frameworks/flex-config.xml' />
         <source-path path-element='${FLEX_HOME}/frameworks'/>
         <!-- source paths -->
         <compiler.source-path path-element='${srcpath.dir}'/>
        <!-- add external libraries -->
        <include-libraries file='${libs.dir}' />
    </mxmlc>
</target>

If you go to the command line and type ‘ant build-test—module’, it will build the module and copy it to our bin-debug directory. Now, we just need to tell the Fluint task to go find that module and run the test.  Here’s the target:

<target name='test'>
    <fluint
       debug='true'
       headless='true'
       failOnError='true'
       workingDir='${fluint.dir}'
       testRunner='${fluint.dir}/FluintAIRTestRunner.exe'
       outputDir='${report.dir}'>

       <fileset dir='${deploypath.dir}/tests'>
          <include name='**/*TestModule.swf'/>
       </fileset>
    </fluint>
</target>

You may have noticed the two new build variables: fluint.dir and report.dir.  The Fluint task needs to know where the FluintAirTestRunner.exe file resides, which is the former.  The latter tells fluint where to write the report of how the tests faired.   I added this to the bottom of the build.properties file:

#Fluint vars
fluint.dir = C:/Program Files (x86)/FluintAIRTestRunner
report.dir = ${basedir}/reports

Now, running ‘ant test’ at the command line will run our test and generate the test report.  It’s an XML file that looks like:

<testsuites status="true" failureCount="0" errorCount="0" testCount="1">
  <testsuite name="tests.ZoomButtonTestCase" errors="0" failures="0" tests="1" time="0.237">
    <properties/>
    <testcase name="testZoomIn" time="0.237" className="tests.ZoomButtonTestCase"/>
  </testsuite>
</testsuites>

So, our ant file is now running our tests.  We are well on our way to continuous integration with Flex.  Coming up, we’ll need to get some of the hardcoding out of the build file, look at our project directory structure, format our test reports to be more readable, and bring Hudson into the mix.  We have a lot to do.

I hope you find this useful.


Continuous Integration with Flex, Hudson, and ArcGIS Server, Part 2

(Part 1Part 3Part 4)

In the last post, I talked about the very basics of getting a Flex project going with Flex Builder and ant.  The current ant script we have just builds the SWF file, which leaves out a bunch of files that Flex Builder uses, and that is bad as defined by what I am trying to blog about here.  So, let’s get going on pulling over the rest of the files.  Well, at least, the important ones.  The most important one is likely the HTML file that wraps our SWF.  If we are going to deploy this SWF in any meaningful (read: web) manner, we need that HTML file.  Actually, we need one that we make ourselves and pull into our build process, but that will left as an assignment for the reader.  For the sake of this series, we’ll just reuse the HTML file that Flex Builder was kind enough to generate.

In the default Flex Builder project, one of the folders is html-template, that looks like:

image

Not much to it, really.  I am not gonna go into the meat of what the files are, if you want that go here.  In a nutshell, what we need to do here is copy this whole directory to our output folder (our ${deploy.dir} for you sharp ones following along).  There is a slight caveat here, tho, and that’s the contents of the index.template.html.  If you open that file in a text editor, you’ll see snippets like:

AC_FL_RunContent(
        "src", "playerProductInstall",
        "FlashVars", "MMredirectURL="+MMredirectURL+'&MMplayerType='+MMPlayerType+'&MMdoctitle='+MMdoctitle+"",
        "width", "${width}",
        "height", "${height}",
        "align", "middle",
        "id", "${application}",
        "quality", "high",
        "bgcolor", "${bgcolor}",
        "name", "${application}",
        "allowScriptAccess","sameDomain",
        "type", "application/x-shockwave-flash",
        "pluginspage", "http://www.adobe.com/go/getflashplayer"
    );

where you may notice the now familiar ${} variables.  So, now our simple copy has turned into a copy and replace all the ${} tokens with their appropriate values.  Nothing is ever as simple as it should be.

The good news is that it’s not so hard either.  Breaking down what we need to do, I think it looks something like this:

  1. Copy the html-template contents.
  2. Parse the index.template.html and replace the tokens.

Let’s create our ant task to get this done.  I’ll it create-html-wrapper, and it looks like this:

<target name='create-html-wrapper'>
    <copy todir='${deploypath.dir}' overwrite="true">
      <fileset dir='${wrapper.dir}'>
         <exclude name='**/index.template.html' />
     <exclude name='**/index.template.html.svntmp' />
         <exclude name='.svn' />
      </fileset>
</copy>

The copy ant task is a standard task from the ant library.  We are excluding some items here:  we don’t want any source control artifacts to end up in the deploy directory.  I am using subversion here, so I explicitly exclude those.  The one that may look odd is the exclusion of index.template.html.  The reason that file is excluded is because we want to rename it.  I don’t want my HTML file to be named index.template.html, do you?

(NOTE:  The Flex ant tasks include an html-wrapper task, but I found it lacking for reasons that now escape me.  I think the parsing of the HTML files was either not there or crappy.  Either way, I dumped it for some very valid reason, I am sure.)

For the index.template.html, we can use another cool ant ask that uses regular expressions to replace content within a file.  The name of that task is replaceregexp, and it’s dead sexy.  Here is the copy command, and it’s a bit of a doozy.  I am not gonna go through each token, as that would be REALLY boring (not super exciting like the rest of this post)

<!-- Copy and rename the index.template.html -->
<copy file='${wrapper.dir}/index.template.html'
tofile='${html.file}' />
<!-- Copy and rename the index.template.html --> <copy file='${wrapper.dir}/index.template.html' tofile='${html.file}' /> <replaceregexp file='${html.file}' flags='gs' match='\$\{width\}' replace='100%'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{height\}' replace='100%'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{title\}' replace='${APP_TITLE}' encoding='utf-8'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{version_major\}' replace='${version.major}'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{version_minor\}' replace='${version.minor}'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{version_revision\}' replace='${version.revision}'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{application\}' replace='${APP_TITLE}'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{bgcolor\}' replace='#FFFFFF'/> <replaceregexp file='${html.file}' flags='gs' match='\$\{swf\}' replace='${application.name}'/>
(BTW, the replaceregexp stuff I blatantly stole from here, which is a GREAT post on using ant with Flex.  Also goes to show you that I am a couple of years behind the cool kids.)

Finally, let’s take another look at our build.properties file, which added quite a bit of properties for this task.

#Copy this file locally to build.properties and change below for your env

# change this to your Flex SDK directory path
FLEX_HOME=C:/Program Files (x86)/Adobe/Flex Builder 3/sdks/3.2.0

# this points to your project's src directory
# {$basedir} is a default variable that can be used in any Antscript
# and it points to the project's root folder [ flex_ant_pt1_Tasks ]
# in this case
srcpath.dir =${basedir}/src

# points to the project's libs directory
libs.dir =${basedir}/libs

# this is the folder we want to publish the swf to
deploypath.dir = ${basedir}/bin-debug

wrapper.dir=${basedir}/html-template

version.major =0
version.minor=9
version.revision = 0
APP_TITLE = Flex Solution
APP_WIDTH = 100%
APP_HEIGHT =100%
locale = en_US
html.file=${deploypath.dir}/index.html
application.name=FlexAGSCI

We have some of our HTML tokens, like width and height, a new directory (wrapper.dir) and the name of our HTML file.  For the ant build, I rename it to index.html because I plan on using ant to actually deploy the application to a web server and I want a default document name to be used.

So, next post will concentrate on adding a unit testing framework and getting ant to run our tests too.  I know the first couple of posts were mongo-basic, but I wanted to lay a bit of a foundation before getting into the cooler stuff.  We’ll get there.  I am about to head off on a mini-vacation, so it might be a week or more before I get to it.  I hope you can manage until then….

Oh, and here’s our build.xml, in full, just for fun:

<project name='Flex Ant Tasks Build Script' default='build-flex-project'>
    <!-- load properties file -->
    <property file='build.properties'/>

    <!-- points to are flexTasks.jar -->
    <taskdef resource='flexTasks.tasks' />    

    <!-- delete and redeploy -->
    <target name='init'>
        <delete includeemptydirs='true'>
            <fileset dir='bin-debug' includes='**\*' excludes='**\.svn'/>
        </delete>

    </target>
    <target name='build-flex-project' depends='init,create-html-wrapper'>
        <mxmlc file='${srcpath.dir}/${application.name}.mxml' output='${deploypath.dir}/${application.name}.swf'>
            <load-config filename='${FLEX_HOME}/frameworks/flex-config.xml'/>
            <include-libraries file='${libs.dir}' />
        </mxmlc>

    </target>

    <target name='create-html-wrapper'>
        <copy todir='${deploypath.dir}' overwrite="true">
              <fileset dir='${wrapper.dir}'>
               <exclude name='**/index.template.html' />
               <exclude name='**/index.template.html.svntmp' />
               <exclude name='.svn' />
              </fileset>
        </copy>
          <!-- Copy and rename the index.template.html -->
         <copy file='${wrapper.dir}/index.template.html'
              tofile='${html.file}' />
          <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{width\}'
              replace='100%'/>
           <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{height\}'
              replace='100%'/>
         <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{title\}'
              replace='${APP_TITLE}'
              encoding='utf-8'/>
         <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{version_major\}'
              replace='${version.major}'/>
         <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{version_minor\}'
              replace='${version.minor}'/>
           <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{version_revision\}'
              replace='${version.revision}'/>
         <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{application\}'
              replace='${APP_TITLE}'/>
          <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{bgcolor\}'
              replace='#FFFFFF'/>
         <replaceregexp
              file='${html.file}'
              flags='gs'
              match='\$\{swf\}'
              replace='${application.name}'/> 

    </target>
</project>