Flex functional testing with FunFx and Cucumber

August 20th, 2009 by Andrea Franz

Cucumber is a great tool I usually use for BDD in my ruby projects, but yesterday I tried it with Flex, and it was very enjoyable. Here a little example on how to test Flex applications with Cucumber.

First of all you need ruby, then you need to install the following gems:

sudo gem install rspec cucumber watir safariwatir funfx

(I used the following codes on mac os x, with ruby 1.8.6, safari 4.0.3 and funx 0.2.2)

After that open Flex Builder and create a new project called CucumberExample and use the following code for your main mxml file:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
	<mx:Number id="index">0</mx:Number>
	<mx:VBox width="300" height="200" backgroundColor="white">
		<mx:HBox width="100%" height="30">			
			<mx:Button id="buttonDecrement" label="-" width="100%" />
			<mx:Button id="buttonIncrement" label="+" width="100%" />
		</mx:HBox>
		<mx:HBox width="100%" height="100%" verticalAlign="middle" horizontalAlign="center">
			<mx:Label id="myLabel" text="{index}"/>			
		</mx:HBox>
	</mx:VBox>
</mx:Application>

As you can see, we have a number, 2 buttons and a label. Now create a directory called features inside your flex project folder and create the first Cucumber feature file, called counter:

CucumberExample/features/counter.feature:

Feature: Counter
  In order to count something
  As a flex rock star
  I want to use my great flex app
 
  Scenario: Increment index
    Given I open my flex app
    And I click the increment button
    Then the label text should be "1"
    When I click the decrement button
    Then the label text should be "0"
    When I click the decrement button
    Then the label text should be "-1"

It’s a test written in pure plain text.

In the features folder create another folder called step_definitions with a blank file called counter_steps.rb, we’ll use it later.

Now open the console, go the to flex project root and run your test:

cd /path/to/CucumberExample
cucumber features

You should see something like this:

...
1 scenario (1 undefined)
7 steps (7 undefined)
0m0.002s

You can implement step definitions for undefined steps with these snippets:

Given /^I open my flex app$/ do
  pending
end
 
Given /^I click the increment button$/ do
  pending
end
 
Then /^the label text should be "([^\"]*)"$/ do |arg1|
  pending
end
 
When /^I click the decrement button$/ do
  pending
end

Before implementing the steps above, we need to add the funfx component to our library.

Download the latest swc file from rubyforge and place it in your CucumberExample/libs folder (I used the version 0.2.2).

Duplicate the bin-debug folder and rename it to test (you have a bin-debug folder after the first time you run the CucumberExample from FlexBuilder).

Inside the test directory create a file called test_server.rb with the following content:

require 'webrick'
 
server = WEBrick::HTTPServer.new :Port => 9852, :DocumentRoot => File.dirname(__FILE__)
trap("INT"){ server.shutdown }
server.start

Now you need a compiled version of your project that includes funfx.We’ll use it just for testing:

build_test.sh

#!/bin/sh
FLEX_SDK_HOME="/Applications/Adobe Flex Builder 3/sdks/3.2.0"
"$FLEX_SDK_HOME/bin/mxmlc" -verbose-stacktraces -include-libraries ./libs/funfx-0.2.2.swc "$FLEX_SDK_HOME/frameworks/libs/automation.swc" "$FLEX_SDK_HOME/frameworks/libs/automation_dmv.swc"  "$FLEX_SDK_HOME/frameworks/libs/automation_agent.swc" -output ./test/CucumberExample.swf -- ./src/CucumberExample.mxml

Run test compile_test.sh

chmod 755 ./build_test.sh
./build_test.sh

Now open another terminal window and start the test server:

cd /path/to/CucumberExample/test
ruby test_server.rb

Open http://localhost:9852/CucumberExample.html with your browser and you should see your flex app.

Now you can come back to your features and implement the step we leave before. Here my implementation:

CucumberExample/features/step_definitions/counter_steps.rb

Given /^I open my flex app$/ do
  open_flex_app
end
 
Given /^I click the (increment|decrement) button$/ do |button|  
  click_button("button#{button.capitalize}")  
end
 
Then /^the label text should be "([^\"]*)"$/ do |text|
  label_text.should == text  
end

open_flex_app, click_button and label_text are methods defined in my env.rb file.

CucumberExample/features/support/env.rb

require "rubygems"
require 'funfx/browser/safariwatir'
 
module FlexWorld
  def open_flex_app
    @browser.goto("http://localhost:9852/CucumberExample.html")
  end
 
  def click_button(button_id)
    @flex_app.button(:id => button_id).click
  end
 
  def label_text
    @flex_app.label(:id => "myLabel").text
  end
end
 
Before do
  @browser  = Watir::Safari.new
  @flex_app = @browser.flex_app("CucumberExample", "CucumberExample")  
end
 
After do
  @browser.close
end
 
World(FlexWorld)

Now you are ready to run your tests. Start the test_server and then the features. You should have an error:

Then the label text should be "1"  # features/step_definitions/counter_steps.rb:9
      expected: "1",
           got: "0" (using ==)

This because we didn’t implement the functionality yet in our flex app. So, change the mxml file with the following:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
	<mx:Number id="index">0</mx:Number>
	<mx:VBox width="300" height="200" backgroundColor="white">
		<mx:HBox width="100%" height="30">			
			<mx:Button id="buttonDecrement" label="-" width="100%" click="index--" />
			<mx:Button id="buttonIncrement" label="+" width="100%" click="index++" />
		</mx:HBox>
		<mx:HBox width="100%" height="100%" verticalAlign="middle" horizontalAlign="center">
			<mx:Label id="myLabel" text="{index}"/>			
		</mx:HBox>
	</mx:VBox>
</mx:Application>

It’s the same code but I added the click action to the increment and decrement buttons. Now compile the new code and run the features again:

./build_test.sh
cucumber features

You shoiuld have the following output, with all the seven steps passed:

Feature: Counter
  In order to count something
  As a flex rock star
  I want to use my great flex app
 
  Scenario: Increment index            # features/counter.feature:6
    Given I open my flex app           # features/step_definitions/counter_steps.rb:1
    And I click the increment button   # features/step_definitions/counter_steps.rb:5
    Then the label text should be "1"  # features/step_definitions/counter_steps.rb:9
    When I click the decrement button  # features/step_definitions/counter_steps.rb:5
    Then the label text should be "0"  # features/step_definitions/counter_steps.rb:9
    When I click the decrement button  # features/step_definitions/counter_steps.rb:5
    Then the label text should be "-1" # features/step_definitions/counter_steps.rb:9
 
1 scenario (1 passed)
7 steps (7 passed)
0m9.781s

If you have any problems you may want to clear the cache from safari.

I used the funfx gem from rubyforge but it’s not up to date. If you want you can get the new code from github, it should have new features.


Testing rails generators with Cucumber

August 11th, 2009 by Andrea Franz

In the last days I switched a lot of old projects to use Cucumber, then I started writing test for Web App Theme. Here I haven’t models or controllers to test, because it’s just a generator, but I found enjoyable the use of plain text features to describe the ThemeGenerator behavior:

Feature: Layout generation
  In order to create a great application
  I should be able to generate a layout with Web App Theme
 
  # script/generate theme
  Scenario: Generate a layout    
    Given I have a new rails app
    And I have no layouts
    And I have no stylesheets
    When I generate a theme
    Then I should have a layout named "application.html.erb"
    And I should have a stylesheet named "web_app_theme.css"
    And I should have a stylesheet named "web_app_theme_override.css"
    And I should have a stylesheet named "themes/default/style.css"
 
  # script/generate theme admin
  Scenario: Generate a layout with a name
    Given I have a new rails app
    And I have no layouts
    And I generate a theme with name "admin"
    Then I should have a layout named "admin.html.erb"
 
  # script/generate theme --theme="drastic-dark"
  Scenario: Generate a layout choosing a theme
    Given I have a new rails app
    And I have no stylesheets
    And I generate a theme choosing the "drastic-dark" theme
    Then I should have a stylesheet named "themes/drastic-dark/style.css"
 
  # script/generate theme --theme=bec --no_layout
  Scenario: Generate only stylesheets without layout
    Given I have a new rails app
    And I have no layouts
    And I generate a theme without layout choosing the "bec" theme
    Then I should have a stylesheet named "themes/bec/style.css"
    But I should not have any layouts
 
  # script/generate theme --app_name="My New Application"
  Scenario: Generate layout with application name
    Given I have a new rails app
    And I have no layouts
    And I generate a theme with application name "My New Application"
    Then the layout "application.html.erb" should have "My New Application" as page title
 
  # script/generate theme --type=sign
  Scenario: Generate layout for signin and signup
    Given I have a new rails app
    And I have no layouts
    And I generate a theme for signin and signup
    Then I should have a layout named "sign.html.erb"
    And I should have a layout named "sign.html.erb" with just a box

Here my steps:

Given /^I have a new rails app$/ do
  generate_rails_app
end
 
Given /^I have no layouts$/ do
  remove_layouts  
end
 
Given /^I have no stylesheets$/ do
  remove_stylesheets
end
 
Given /^I generate a theme$/ do
  generate_layout(:theme)  
end
 
Given /^I generate a theme with name "([^\"]*)"$/ do |name|
  generate_layout(:theme, name)
end
 
Given /^I generate a theme choosing the "([^\"]*)" theme$/ do |theme_name|
  generate_layout(:theme, :theme => theme_name)
end
 
Then /^I should have a layout named "([^\"]*)"$/ do |filename|
  layout_exists?(filename).should be_true  
end
 
Then /^I should have a stylesheet named "([^\"]*)"$/ do |filename|
  stylesheet_exists?(filename).should be_true    
end
 
Given /^I generate a theme without layout choosing the "([^\"]*)" theme$/ do |theme_name|
  generate_layout(:theme, :theme => theme_name, :no_layout => true )
end
 
Then /^I should not have any layouts$/ do
  layouts_count.should == 0
end
 
Given /^I generate a theme with application name "([^\"]*)"$/ do |name|
  generate_layout(:theme, :app_name => name )
end
 
Then /^the layout "([^\"]*)" should have "([^\"]*)" as page title$/ do |layout, title|
  layout_title(layout).should == title
end
 
Given /^I generate a theme for signin and signup$/ do
  generate_layout(:theme, :layout_type => :sign)
end
 
Then /^I should have a layout named "([^\"]*)" with just a box$/ do |layout|
  layout_with_box?(layout).should be_true
end

Basically I create a temp folder that I use as fake rails root, and there I launch the generator. After each feature I remove that folder.

And here my env.rb

$:.unshift(File.dirname(__FILE__) + "/../../rails_generators")
require "rubygems"
require "rails_generator"
require 'rails_generator/scripts/generate'
require "fileutils"
require "theme/theme_generator"
 
web_app_theme_root  = File.join(File.dirname(__FILE__), "/../../")
tmp_rails_app_name  = "tmp_rails_app"
tmp_rails_app_root  = File.join(web_app_theme_root, tmp_rails_app_name)
 
Rails::Generator::Base.append_sources(Rails::Generator::PathSource.new(:plugin, "#{web_app_theme_root}/rails_generators/"))
 
module GeneratorHelpers
  def generate_rails_app
    FileUtils.mkdir(File.join(@app_root))
  end    
 
  def remove_layouts
    FileUtils.rm_rf(File.join(@app_root, "app", "views", "layouts"))
  end
 
  def remove_stylesheets
    FileUtils.rm_rf(File.join(@app_root, "public", "stylesheets"))
  end
 
  def generate_layout(*args)
    options = !args.empty? && args.last.is_a?(Hash) ? args.pop : {}
    options.merge!({:destination => @app_root, :quiet => true})    
    Rails::Generator::Scripts::Generate.new.run(args, options)
  end
 
  def layouts_count
    Dir[File.join(@app_root, "app", "views", "layouts", "**", "*.erb")].size
  end
 
  def layout_exists?(filename)
    File.exists?(File.join(@app_root, "app", "views", "layouts", filename))
  end
 
  def stylesheet_exists?(relative_path)
    File.exists?(File.join(@app_root, "public", "stylesheets", relative_path)).should be_true
  end
 
  def layout_title(layout)
    File.open(File.join(@app_root, "app", "views", "layouts", layout), "r").read.match(/<title>([^<]*)<\/title>/)[1]
  end
 
  def layout_with_box?(layout)
    File.open(File.join(@app_root, "app", "views", "layouts", layout), "r").read =~ %r|<div id="box">|
  end
end
 
Before do
  @app_root = tmp_rails_app_root  
end
 
After do
  FileUtils.rm_rf(tmp_rails_app_root)
end
 
World(GeneratorHelpers)