Tuesday, 17 December 2013

Unit Testing Sencha Touch (or ExtJS) applications using Karma Test Runner, Jasmine, Maven and Jenkins


I wanted to be able to run the unit tests for my Sencha Touch 2.3 application locally as well as being able to run them in my continous integration process using Jenkins.

I decides to  use "Karma - Spectacular Test Runner for JavaScript" because it nicely integrates with Jasmine and offers headless execution using PhantomJS out of the box. If PhantomJS is not enough for you, you can also specify other Browsers.

Karma runs on NodeJS. I am not going to describe how to set it up. This information can be found on the Karma site.

Run the tests locally

Here is the 'karma.config.js' I use for local testing.

I bootstrap the Sencha Touch app using the 'development.js' microloader. Because the microloader is loading all required files dynamically I need to specify a proxy element in the Karma config pointing to a local webserver hosting the development version of the application JS files as well as the Sencha Touch JS files.


module.exports = function (config) {
    config.set({
        // base path, that will be used to resolve files and exclude
        basePath: '',

        // frameworks to use
        frameworks: [ 'jasmine' ],

        // list of files / patterns to load in the browser
        // Files for Sencha Touch development microloader
        files: [
            'src/main/webapp/.sencha/app/microloader/development.js',
            'src/test/javascript/setUp.js',
            'src/test/javascript/**/*.js' 
        ],

        // list of files to exclude
        exclude: [
        ],

        proxies: {
            '/': 'http://localhost:8080/'
        },

        // test results reporter to use
        // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
        reporters: [ 'progress'],

        // web server port
        port: 9876,

        // enable / disable colors in the output (reporters and logs)
        colors: true,

        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR ||
        // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_DEBUG,

        // enable / disable watching file and executing tests whenever any file
        // changes
        autoWatch: true,

        // Start these browsers, currently available:
        // - Chrome
        // - ChromeCanary
        // - Firefox
        // - Opera (has to be installed with `npm install karma-opera-launcher`)
        // - Safari (only Mac; has to be installed with `npm install
        // karma-safari-launcher`)
        // - PhantomJS
        // - IE (only Windows; has to be installed with `npm install
        // karma-ie-launcher`)
        browsers: [ 'PhantomJS' ],

        // If browser does not capture in given timeout [ms], kill it
        captureTimeout: 60000,

        // Continuous Integration mode
        // if true, it capture browsers, run tests and exit
        singleRun: false
    });
};

If you do not want to use the 'development.js' microloader to bootstrap Sencha Touch one can also point to the single JS file created by a Sencha Command test or production build. Then one does not need the proxy configuration.


        // Files for Sencha Touch test build
        files: [
            'src/main/webapp/build/testing/mae/app.js',
            'src/test/javascript/setUp.js',
            'src/test/javascript/**/*.js'
        ],

The advantage of the 'development.js' microloader approach is that you can changes your JS files and test them immediately. The drawback is that you need a web server running.

The advantage of pointing to the result of a Sencha Command build is that you do not need to have a webserver running. The drawback is that you need to rebuild after every change to your JS files to be able to test them.

You have probably noticed the file 'setup.js' that I include before the real test files.

1) This creates an element in the DOM which Sencha Touch expects
2) Delays the start of the tests till the Sencha Application is ready


// We need to add an element with the ID 'appLoadingIndicator' because [app.js].launch() is expecting it and tries to remove it
var myDiv = document.createElement("div");
myDiv.setAttribute("id", "appLoadingIndicator");
document.getElementsByTagName("body")[0].appendChild(myDiv);

// Karma normally starts the tests right after all files specified in 'karma.config.js' have been loaded
// We only want the tests to start after Sencha Touch/ExtJS has bootstrapped the application.
// 1. We temporary override the '__karma__.loaded' function
// 2. When Ext is ready we call the '__karma__.loaded' function manually
var karmaLoadedFunction = window.__karma__.loaded;
window.__karma__.loaded = function () {};

Ext.onReady(function () {
    console.info("Starting Tests ...");
    window.__karma__.loaded = karmaLoadedFunction;
    window.__karma__.loaded();
});

Another nice thing about Karma is that there is integration into the Webstorm IDE, i.e. you can run and even debug you tests from within Webstorm

Run the tests on Jenkins

I am using a different 'karma.config.js' for running tests within Jenkins.

It uses a "junitReporter" and has "singleRun=true" specified. It loads the "app.js" file  created by Sencha Command containing the Sencha Touch and my application JS files.


module.exports = function(config) {
 config.set({

  // base path, that will be used to resolve files and exclude
  basePath : '',

  // frameworks to use
  frameworks : [ 'jasmine' ],

  // list of files / patterns to load in the browser
  files : [ 'javascript/app.js','javascript/setUp.js',
    'javascript/**/*.js' ],

  // list of files to exclude
  exclude : [ 
  ],

  // test results reporter to use
  // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
  reporters : [ 'dots', 'junit' ],

  junitReporter : {
   outputFile : 'karma-test-results.xml'
  },

  // web server port
  port : 9876,

  // enable / disable colors in the output (reporters and logs)
  colors : true,

  // level of logging
  // possible values: config.LOG_DISABLE || config.LOG_ERROR ||
  // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
  logLevel : config.LOG_INFO,

  // enable / disable watching file and executing tests whenever any file
  // changes
  autoWatch : false,

  // Start these browsers, currently available:
  // - Chrome
  // - ChromeCanary
  // - Firefox
  // - Opera (has to be installed with `npm install karma-opera-launcher`)
  // - Safari (only Mac; has to be installed with `npm install
  // karma-safari-launcher`)
  // - PhantomJS
  // - IE (only Windows; has to be installed with `npm install
  // karma-ie-launcher`)
  browsers : [ 'PhantomJS' ],

  // If browser does not capture in given timeout [ms], kill it
  captureTimeout : 60000,

  // Continuous Integration mode
  // if true, it capture browsers, run tests and exit
  singleRun : true
 });
};

I am using Maven to build my app:

1) Sencha Command builds the app.js in the 'generate-resources' phase
2) I specify where my "test resources" are so Maven can copy them to the target folder
3) In the "test" phase Karma runs the tests.

Here is a snippet from my POM:


<build>
 <!-- Specify the location of the test resources and JS files -->
 <testResources>
  <testResource>
   <directory>${basedir}/src/test/resources</directory>
  </testResource>
  <testResource>
   <directory>${basedir}/src/main/webapp/build/${senchaBuildEnvironment}/mae</directory>
   <includes>
    <include>app.js</include>
   </includes>
   <targetPath>javascript</targetPath>
  </testResource>
  <testResource>
   <directory>${basedir}/src/test/javascript</directory>
   <targetPath>javascript</targetPath>
  </testResource>
 </testResources>
 <plugins>
  <!-- Sencha Command will build our web-application -->
  <plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>exec-maven-plugin</artifactId>
   <version>1.2.1</version>
   <executions>
    <execution>
     <id>SenchaCommand</id>
     <phase>generate-resources</phase>
     <goals>
      <goal>exec</goal>
     </goals>
     <configuration>
      <workingDirectory>${basedir}/src/main/webapp</workingDirectory>
      <executable>/sencha/Cmd/4.0.0.203/sencha</executable>
      <arguments>
       <argument>app</argument>
       <argument>build</argument>
       <argument>${senchaBuildEnvironment}</argument>
      </arguments>
     </configuration>
    </execution>
  <!-- Karma runs the JS tests in the 'test' phase -->       
    <execution>
     <id>Karma Test Runner</id>
     <phase>test</phase>
     <goals>
      <goal>exec</goal>
     </goals>
     <configuration>
      <workingDirectory>${basedir}/target/test-classes</workingDirectory>
      <executable>node</executable>
      <arguments>
       <argument>${node.modules.home}/karma/bin/karma</argument>
       <argument>start</argument>
      </arguments>
     </configuration>
    </execution>
   </executions>
  </plugin>
  ...

I have different profiles for test and production builds, .e.g.
     
<profile>
 <id>production</id>
 <activation>
  <property>
   <name>env</name>
   <value>production</value>
  </property>
 </activation>
 <properties>
  <senchabuildenvironment>production</senchabuildenvironment>
 </properties>
</profile>

Now Jenkins simply calls "mvn -Denv=production clean test".

I configured Jenkings to pick up the "karma-test-results.xml" file to display the test results:

**/target/surefire-reports/*.xml, **/target/test-classes/karma-test-results.xml

1 comment:

  1. I have been trying to get this to work for a long time, thank you for sharing.

    ReplyDelete