Wednesday, August 17, 2011

Web Application Testing Automation

Architecture Choices

We have chosen the following architecture for our web application testing:
  • Primary test client is a Windows 2003 server running in Amazon's EC2 environment which if fully patched and has Internet Explorer 8, and the latest versions of Google Chrome and Firefox.
  • SeleniumHQ with test scripts written in PHP since it is already a core technology that we used to build the site.
  • The suite of tests developed on the primary test client will be written in such a way that they can also be copied and run from Windows and Ubuntu Linux machines located outside of the Amazon environment.
  • In the future, testing will be expanded to include IE9 and possibly additional browsers and versions.
We feel this will give us a testing process with the following qualities:
  • Comprehensive testing capability
  • Cost Effective
  • Reliable
  • Accessible by many from anywhere
Sources for Inspirations, Ideas and getting started

Selenium Server Configuration

The following was captured during setup of the Selenium Server on the primary test client...

Before installing the Selenium Server we need to install a recent version of Java from http://www.java.com/en/download/manual.jsp.

Now download Selenium Server (2.4.0) from the Selenium Downloads page and put it into a folder where you are likely to find should you need to down the road.

Create a batch file containing the following in the same folder that you put the Selenium Server jar file into:
java -jar selenium-server-standalone-2.4.0.jar -trustAllSSLCertificates 

Create a shortcut to the above batch file in a convenient location.  When the Selenium Server is running it will occupy a command window that can be shut down using Ctrl-C when you are done with it.

Install PHP
Download the zip file from http://windows.php.net/download/ and unzip it into C:\Program Files\PHP\.
Copy php.ini-development to php.ini.
C:\Program Files\PHP>copy php.ini-development php.ini
        1 file(s) copied.


Install PEAR
Make sure you install the latest version of PEAR by first replacing the C:\PROGRA~1\PHP\PEAR\go-pear.phar file with one obtained from http://pear.php.net/go-pear.phar.  Your output may not look like mine because I ran mine twice, first with an out of date version of the PEAR installer.
C:\PROGRA~1\PHP>php PEAR\go-pear.phar

Are you installing a system-wide PEAR or a local copy?
(system|local) [system] :

Below is a suggested file layout for your new PEAR installation.  To
change individual locations, type the number in front of the
directory.  Type 'all' to change all of them or simply press Enter to
accept these locations.

 1. Installation base ($prefix)                   : C:\PROGRA~1\PHP
 2. Temporary directory for processing            : C:\PROGRA~1\PHP\tmp
 3. Temporary directory for downloads             : C:\PROGRA~1\PHP\tmp
 4. Binaries directory                            : C:\PROGRA~1\PHP
 5. PHP code directory ($php_dir)                 : C:\PROGRA~1\PHP\pear
 6. Documentation directory                       : C:\PROGRA~1\PHP\docs
 7. Data directory                                : C:\PROGRA~1\PHP\data
 8. User-modifiable configuration files directory : C:\PROGRA~1\PHP\cfg
 9. Public Web Files directory                    : C:\PROGRA~1\PHP\www
10. Tests directory                               : C:\PROGRA~1\PHP\tests
11. Name of configuration file                    : C:\WINDOWS\pear.ini
12. Path to CLI php.exe                           : C:\PROGRA~1\PHP

1-12, 'all' or Enter to continue:
Beginning install...
Configuration written to C:\WINDOWS\pear.ini...
Initialized registry...
Preparing to install...
installing phar://C:/Program Files/PHP/PEAR/go-pear.phar/PEAR/go-pear-tarballs/A
rchive_Tar-1.3.7.tar...
installing phar://C:/Program Files/PHP/PEAR/go-pear.phar/PEAR/go-pear-tarballs/C
onsole_Getopt-1.3.0.tar...
installing phar://C:/Program Files/PHP/PEAR/go-pear.phar/PEAR/go-pear-tarballs/P
EAR-1.9.4.tar...
installing phar://C:/Program Files/PHP/PEAR/go-pear.phar/PEAR/go-pear-tarballs/S
tructures_Graph-1.0.4.tar...
installing phar://C:/Program Files/PHP/PEAR/go-pear.phar/PEAR/go-pear-tarballs/X
ML_Util-1.2.1.tar...
install ok: channel://pear.php.net/Archive_Tar-1.3.7
install ok: channel://pear.php.net/Console_Getopt-1.3.0
install ok: channel://pear.php.net/Structures_Graph-1.0.4
install ok: channel://pear.php.net/XML_Util-1.2.1
install ok: channel://pear.php.net/PEAR-1.9.4
PEAR: Optional feature webinstaller available (PEAR's web-based installer)
PEAR: Optional feature gtkinstaller available (PEAR's PHP-GTK-based installer)
PEAR: Optional feature gtk2installer available (PEAR's PHP-GTK2-based installer)

PEAR: To install optional features use "pear install pear/PEAR#featurename"

** WARNING! Old version found at C:\PROGRA~1\PHP, please remove it or be sure to
 use the new c:\progra~1\php\pear.bat command

The 'pear' command is now at your service at c:\progra~1\php\pear.bat

** The 'pear' command is not currently in your PATH, so you need to
** use 'c:\progra~1\php\pear.bat' until you have added
** 'C:\PROGRA~1\PHP' to your PATH environment variable.

Run it without parameters to see the available actions, try 'pear list'
to see what packages are installed, or 'pear help' for help.

For more information about PEAR, see:

  http://pear.php.net/faq.php
  http://pear.php.net/manual/

Thanks for using go-pear!



* WINDOWS ENVIRONMENT VARIABLES *
For convenience, a REG file is available under C:\PROGRA~1\PHPPEAR_ENV.reg .
This file creates ENV variables for the current user.

Double-click this file to add it to the current user registry.


C:\PROGRA~1\PHP>

Double-click C:\PROGRA~1\PHPPEAR_ENV.reg as instructed above.

Install PHPUnit

Issue the 4 commands highlighted below as instructed on http://www.phpunit.de/manual/3.6/en/installation.html.  Again your output may be different because what I show below is not from a virgin install. I was only able to get the beta version to install without errors, hence the "-beta" at the end of the fourth command.

C:\PROGRA~1\PHP>pear channel-discover pear.phpunit.de
Adding Channel "pear.phpunit.de" succeeded
Discovery of channel "pear.phpunit.de" succeeded

C:\PROGRA~1\PHP>pear channel-discover components.ez.no
Adding Channel "components.ez.no" succeeded
Discovery of channel "components.ez.no" succeeded

C:\PROGRA~1\PHP>pear channel-discover pear.symfony-project.com
Adding Channel "pear.symfony-project.com" succeeded
Discovery of channel "pear.symfony-project.com" succeeded

C:\PROGRA~1\PHP>pear install phpunit/PHPUnit-beta
phpunit/PHPUnit can optionally use PHP extension "curl"
phpunit/PHPUnit can optionally use PHP extension "dbus"
pear/HTTP_Request2 can optionally use PHP extension "curl"
pear/HTTP_Request2 can optionally use PHP extension "fileinfo"
pear/HTTP_Request2 can optionally use PHP extension "openssl"
downloading PHPUnit-3.5.14.tgz ...
Starting to download PHPUnit-3.5.14.tgz (118,697 bytes)
..........................done: 118,697 bytes
downloading XML_RPC2-1.1.1.tgz ...
Starting to download XML_RPC2-1.1.1.tgz (68,431 bytes)
...done: 68,431 bytes
downloading HTTP_Request2-2.0.0RC1.tgz ...
Starting to download HTTP_Request2-2.0.0RC1.tgz (95,353 bytes)
...done: 95,353 bytes
downloading Net_URL2-0.3.1.tgz ...
Starting to download Net_URL2-0.3.1.tgz (8,488 bytes)
...done: 8,488 bytes
install ok: channel://pear.php.net/Net_URL2-0.3.1
install ok: channel://pear.php.net/HTTP_Request2-2.0.0RC1
install ok: channel://pear.php.net/XML_RPC2-1.1.1
install ok: channel://pear.phpunit.de/PHPUnit-3.5.14

C:\PROGRA~1\PHP>

Install Firefox and the Selenium IDE Add-On
Install Firefox (http://www.mozilla.com/en-US/firefox/) and the Selenium IDE add-on for it (Tools=>Add-Ons, search for "Selenium IDE").  Version can be an issue.  You need to ensure that the end result is a version of Selenium IDE that supports exporting to PHPUnit.

Running Tests
The following file is named IWA_Test.php which is located in the "H:\Program Files\PHP\" directory and was created largely by collecting and editing code snippets recorded in the Firefox Selenium IDE add-on.  This example runs tests on Chrome, Firefox and IE7.  Make sure you start the  Selenium Server before running the tests.  Run the tests in a command session from the above mentioned directory by typing "phpunit IWA_Test.php".

<?php

//////////////////////////////////////////////////////
// Run from DOS prompt using:  phpunit IWA_Test.php //
//////////////////////////////////////////////////////

  date_default_timezone_set("America/New_York");
  require_once 'PHPUnit\Extensions\SeleniumTestCase.php';

  class WebTest extends PHPUnit_Extensions_SeleniumTestCase {

    // Set up the browsers that we want to test in
    public static $browsers = array(
      array(
        'name'    => 'Google Chrome on Windows',
        'browser' => '*googlechrome H:\Documents and Settings\dave\Local Settings\Application Data\Google\Chrome\Application\chrome.exe',
        'host'    => 'localhost',
        'port'    => 4444,
        'timeout' => 30000,
      ),
      array(
        'name'    => 'Firefox on Windows',
        'browser' => '*firefox H:\Progra~1\Mozill~1\firefox.exe',
        'host'    => 'localhost',
        'port'    => 4444,
        'timeout' => 30000,
      ),
      array(
        'name'    => 'Internet Explorer 7 on Windows',
        'browser' => '*iexplore',
        'host'    => 'localhost',
        'port'    => 4444,
        'timeout' => 30000,
      )
    );

    protected function setUp() {
      $this->setBrowserUrl('https://www.industrialwebapps.com/');
    }

    public function test_IWA() {
      /////////////////////////////////////////////
      // Determine which browser is being tested //
      /////////////////////////////////////////////
      $tmp = serialize($this);
      $chrome_pos   = strpos($tmp, 'Google Chrome on Windows');
      $firefox_pos  = strpos($tmp, 'Firefox on Windows');
      $iexplore_pos = strpos($tmp, 'Internet Explorer 7 on Windows');
      if($chrome_pos   < $firefox_pos && $chrome_pos   < $iexplore_pos) { $curBrowser = 'GoogleChrome'; }
      if($firefox_pos  < $chrome_pos  && $firefox_pos  < $iexplore_pos) { $curBrowser = 'Firefox'; }
      if($iexplore_pos < $firefox_pos && $iexplore_pos < $chrome_pos)   { $curBrowser = 'IE7'; }

      echo PHP_EOL.PHP_EOL.PHP_EOL.'============== '.$curBrowser.' ==============='.PHP_EOL.PHP_EOL.PHP_EOL;


      /////////////////////
      // Request Account //
      /////////////////////
      echo '  Request Account...'.PHP_EOL;
      // Build a testUser with a name that should be unique
      $testUser = $curBrowser."_".date('dHis');
      $this->open("/request_account_form.php");
      $this->type("css=input[name=user_name]", $testUser);
      $this->type("css=input[name=first_name]", $testUser);
      $this->type("css=input[name=last_name]", $testUser);
      $this->type("css=input[name=email]", $testUser."@industrialwebapps.com");
      $this->type("phone_number", "123-456-7890");
      $this->type("password", "9/3=Two_");
      $this->type("password2", "9/3=Two_");
      $this->fireEvent("password2", "keyup");
      for ($second = 0; ; $second++) {
        if ($second >= 60) $this->fail("timeout");
        try {
          if ($this->isElementPresent("css=select[name=company]")) break;
        }
        catch (Exception $e) {}
        sleep(1);
      }
      $this->select("css=select[name=company]", "label=IndustrialWebApps.com");
      $this->type("css=input[name=selected_comp]", 'IndustrialWebApps.com');
      $this->select("timezone", "label=-04:00 ==> Eastern Daylight Time");
      $this->fireEvent("css=input[name=user_name]",  "blur");
      sleep(2.1);
      $this->click("btn_submit");
      $this->waitForPageToLoad("30000");
      $this->assertTrue($this->isTextPresent("exact:Getting Started"));
      $this->assertTrue($this->isTextPresent("exact:When is Dave available?"));


      ///////////
      // Login //
      ///////////
      echo '  Login...'.PHP_EOL;
      $this->open("/login_form.php");
      $this->type("user_name", $testUser);
      $this->type("password", "9/3=Two_");
      sleep(2.1);
      $this->click("css=input[type=submit]");
      $this->waitForPageToLoad("30000");
      $this->assertTrue($this->isElementPresent("css=h1"));
      $this->assertTrue($this->isTextPresent("Getting Started"));

      //////////////////
      // Edit Profile //
      //////////////////
      echo '  Edit Profile...'.PHP_EOL;
      // Change every value
      $this->open("/edit_profile_form.php");
      $this->type("firstname", $testUser."x");
      $this->type("css=input[name=last_name]", $testUser."x");
      $this->type("css=input[name=email]", $testUser."@industrialwebapps.comx");
      $this->type("phone_number", "123-456-7890x");
      $this->select("timezone", "label=-08:00 ==> Pacific Standard Time");
      sleep(2.1);
      $this->click("btn_submit");
      $this->waitForPageToLoad("30000");
      // Confirm that the changes were made
      $this->assertTrue($this->isElementPresent("css=h1"));
      $this->assertTrue($this->isTextPresent($testUser."x"));
      $this->assertTrue($this->isTextPresent($testUser."x"));
      $this->assertTrue($this->isTextPresent($testUser."@industrialwebapps.comx"));
      $this->assertTrue($this->isTextPresent("123-456-7890x"));
      $this->assertTrue($this->isTextPresent("Pacific Standard Time"));

      /////////////////////
      // Change Password //
      /////////////////////
      echo '  Change Password...'.PHP_EOL;
      $this->open("/change_passwd_form.php");
      $this->type("password", "6/2=Two+");
      $this->type("password2", "6/2=Two+");
      $this->fireEvent("password", "keyup");
      $this->fireEvent("password2", "keyup");
      sleep(2.1);
      $this->click("btn_submit");
      $this->waitForPageToLoad("30000");
      $this->assertTrue($this->isTextPresent("exact:Password Change"));
      $this->assertTrue($this->isTextPresent("exact:Password successfully changed."));
      $this->assertTrue($this->isTextPresent("exact:A password change notification has been sent to your configured email address."));
      $this->assertTrue($this->isTextPresent("exact:When is Dave available?"));

      ////////////
      // Logout //
      ////////////
      echo '  Logout...'.PHP_EOL;
      $this->click("link=Logout");
      $this->waitForPageToLoad("30000");
      $this->assertTrue($this->isElementPresent("css=h1"));
      $this->assertTrue($this->isTextPresent("exact:User Login"));
      $this->assertTrue($this->isTextPresent("exact:When is Dave available?"));

    }

  }
?>

Sunday, August 14, 2011

Cancel file upload to iframe

This was a lot more difficult to find that I thought it would be so I'll capture it here in the hopes that is helps us (or anyone else) next time it is needed.

Situation: We use a hidden iframe as the target of the form that we are doing a file upload from. The contribution to this post by Steve Wasiura is what finally got me unstuck.

$('button#cancelUpload').click(function() {
    var amfIfrm = document.getElementById('amf_ifr');
    if($.browser.msie) { // Internet Explorer
      amfIfrm.contentWindow.document.execCommand('Stop');
    } else { // Other browsers
      amfIfrm.contentWindow.stop();
    }
  });

Thursday, August 11, 2011

Tablesorter custom parser for date format "d-mmm-yyyy h:mm:ss A"

I now need to sort full timestamps in my preferred format so needed another parser the handle dates like:
    "2-Mar-2010 2:12:13 PM".

var months = {};
    months["JAN"] = "01";
    months["FEB"] = "02";
    months["MAR"] = "03";
    months["APR"] = "04";
    months["MAY"] = "05";
    months["JUN"] = "06";
    months["JUL"] = "07";
    months["AUG"] = "08";
    months["SEP"] = "09";
    months["OCT"] = "10";
    months["NOV"] = "11";
    months["DEC"] = "12"; 
    ts.addParser({
      id: 'drbDateLong',
      is: function(s) { return false; }, 
      format: function(s) {
        s = '' + s; //Make sure it's a string
        //                  d(1-31) - mmm(Jan-Dec) - yyyy    h(1-12):  mm   :  ss     AM/PM 
        var hit = s.match(/(\d{1,2})-([A-Za-z]{3})-(\d{4}) (\d{1,2}):(\d{2}):(\d{2}) ([A-Za-z]{2})/);
        if (hit && hit.length == 8) {
          if(hit[1].length == 1) { hit[1] = "0"+hit[1]; }
          if(hit[7].toUpperCase() == "PM") { hit[4] = String(parseInt(hit[4])+12); }
          if(hit[4].length == 1) { hit[4] = "0"+hit[4]; }
          return hit[3]+months[hit[2].toUpperCase()]+hit[1]+hit[4]+hit[5]+hit[6];
        } else { return s; }
      },
      type: 'text'
    });

Again, many thanks to Christian Bach and this post by MorningZ.

Monday, August 1, 2011

Tablesorter modification to handle checkboxes

Needing to sort tables that contain checkboxes is a relatively common need but despite this I was not able to find a custom developed parser for tablesorter so I rolled my own.

ts.addParser({
  id: 'checkbox',
  is: function(s) { return false; },
  format: function(s, table, cell) { return $(cell).find('input[type*=checkbox]').is(':checked'); }, 
  type: 'numeric'
});