Friday, December 7, 2012

Easy Testing with Application Craft and QUnit

&

Background

This is one of a series of blogs I'm writing as I build Easy Peasy Task List.  If you want to know about the other related blogs, visit Status of discovery / learning exercises.

Overview

I am in the process of writing a TaskList using Application Craft.  This blog is about learning how to do unit and intergration test with ApplicationCraft.  Unfortunatly ApplicationCraft doesn't provide a testing framework.  So I did some searching around for a javascript testing framework and found QUnit.  This testing framework was originally written by John Resig of JQuery frame.  I figured since AC is written using JQuery Mobile that this would be a reasonable testing framework for my purposes.

Video

The following video gives a brief overview of how AC was utilized to drive the tests and how to incorporate QUnit.  

Live app

The live app can be run from here

Source code

As usual, you can download all the code as a ApplicationCraft project from my github account for TaskList.

Tutorial

So to start off we will look at the Step 3 - Unit Test project and how it is configured.  If you watch the video it will help alot.

Setting up the Step 3-  Unit Test App

  • On the testing app page, set the property "Preload Files" to use http://code.jquery.com/qunit/qunit-git.js and http://code.jquery.com/qunit/qunit-git.css
  • On the testing app page drop a HTML component and add the following which is used by QUnit to display the test results.  
    <h1 id="qunit-header">QUnit Test Suite</h1>
    <h2 id="qunit-banner"></h2>
    <div id="qunit-testrunner-toolbar"></div>
    <h2 id="qunit-userAgent"></h2>
    <div id="qunit-testresult"></div>
    <ol id="qunit-tests"></ol>
    <div id="qunit-fixture">
          <div id="container"></div>
    </div>
    
    
    

  • Be sure to enable the HTML component for Resizing.
  • Add an Embed App widget (See Toolbox -> Advanced).  Once you drop that widget set the property Embed App to the "Step 2 - Persistence" app.
  • Select the AppEmbed component and in the Events tab, select the On Started so that a function is created.

Our first test

A little prep work in the SUT (System Under Test - in this case Step 2 - Persistence app).


This first test will demonstrate how to access a client side function in the embedded app.  In the Step 2 - Persistence app (the app being tested) there is a silly function I've added call "sayHi".  Now just how does the parent app (Step 3 - Unit Testing) get access to that embedded app's function?  Well, the trick is to set a global object with that function as a member.  The following code demonstrates how an Embedded App function is made available for testing.
//Silly function for demonstration purpose
function sayHi(foo) {
    return "Hi " + foo;
}

//Set global object
var gObj = {};

//Set the functions in the global object
gObj.sayHi = sayHi;

gObj.fillZingerChart = fillZingerChart;

//Make the object available 
app.setGlobalData('gObj',gObj);

The function "sayHi" is defined on lines 2-4.  It merely returns the parameter prepended with "Hi ".

On line 7, there is a object defined.  On lines 10 & 12, that object is updated to have two functions: 1) sayHi and 2) fillZingerChart.  These are the two client side functions that I want to test so I define them to this object.

On line 15 that object gObj is set into the global data with the key 'gObj'.  We'll see later how the unit testing code can now access that gObj and invoke these client side functions.

The actual client test code

The following code is entered into the embedded on start function as shown below

function handler_appEmbed_onStarted(){
    /**
     * Simple client side test
     */
    test('call client side function',1, function() {
        var gObj = app.childApp('appEmbed').getGlobalData('gObj');
        var rtn = gObj.sayHi('Barton');
        equal(rtn,'Hi Barton','sayHi should say Hi Barton');    
    });
So line 5 starts the test case.  The first parameter to "test" is a description of what the test will do.  It will show up later in the test results that QUnit provides.  The second parameter, 1 in this case, says how many assertions will be done.  Then a callback function is provided that will be executed when the test is run.

On line 6, we reference the embedded app and get the Global Data value for the key 'gObj' which we set earlier in the client/embedded app.

On line 7 we invoke the "sayHi" function and pass a parameter and save the return value.

On line 8 we assert that the return value is correct.  The first parm is the actual value, the second is the expected and the third value is a description of what the test is asserting.

There you have it - our first unit test run from within Application Craft to test Application Craft.  The code is external to the code we're testing and all the features of being a project within Application Craft are ours for free: source control, debugging, the IDE, the editor, etc.

Let's test the SSJS code

In this next bit of testing we are going to invoke some SSJS code.  Now this gets a little more complex because you have to let the test run asyncronously.  So I want to call three methods: 1) clear the log 2) write to the log 3) return the logs.  I want to confirm that if I send only one log, that I get only one log.  

Note that this test also is running in the embedded on Started method so that all the "plumbing" is in place.
/**
     * Simple server side only test 
     */
    test('test log get cleared and only 1 log is added',3,function() {
        stop();
        //Clear the log 
        app.childApp('appEmbed').callSSJ('clearLog', function(error,data){
            //Set one log object
            app.childApp('appEmbed').callSSJ('setLog', function(error,data) {
                //Get all the logs
                app.childApp('appEmbed').callSSJ('getLog',function(error,data) {
                    start();
                    equal(data.length, 1, 'only one log should be returned');
                    equal(data[0].id, 1, 'id should be 1');
                    ok(data[0].time > 1, 'time should have a value');
                },[true]);
            },[{id:1},true]);   
        },[]);
    });

The test is defined starting on line 4 and it expects to have 3 assertions which are on lines 13-15. In the callback of the test, on line 5, the tests are stopped because we're doing async calls.  The first call to the server is on line 7 which clears the log.  In the callback of that function, on line 9 a call is made to "setLog".  On the callback of that function call on line 11, a call is made to get the log.  Within the callback function we start the QUnit testing again with a "start" on line 12.  Lines 13-15 assert that the logs returned are of length 1 and the data contains the "id" and "time" attributes.

Combining SSJS and Client side testing

This next bit of code calls the SSJS function to "startTesting" which returns the logs from the test run.

    /**
     * Combination SSJS & Client side test
     */
    test('fill Zinger chart data',7, function() {
        stop();
        //Run 1 series of CRUD tests
        app.childApp('appEmbed').callSSJ('startTesting', function(error,data) {
            start();
            equal(data.length,10,'there should be 10 logs');
            
            //Start client side test
            var gObj = app.childApp('appEmbed').getGlobalData('gObj');
            var jsonData = gObj.fillZingerChart('test',data);
            
            //Validate the title
            var title = jsonData.graphset[0].title.text;
            var tokens = title.split(' ');
            equal(tokens[0],'test','title should be test');
            
            //Validate there are only 4 series (CRUD) and one value per series
            equal(jsonData.graphset[0].series.length, 4, 'there should be 4 series');
            for (var row = 0; row < 4; row++) {
                equal(jsonData.graphset[0].series[row].values.length, 1, 'there should only be one series in '
                + jsonData.graphset[0].series[row].text);
            }
        },[1,app.childApp('appEmbed').getValue('databaseRadio').value,false,false]);

          
    });
 
On line 4, we provide a description for the test and state that there will be 7 assertions.  In the callback we immediately stop all testing on line 5.  On line 7 we call the SSJS function "startTesting" and provide the required parameters on line 26.  Note on line 26 how the embedded apps component is referenced to obtain the DB connection string.

In the callback of the "startTesting" SSJS call, on line 8 we start our testing again and validate that there should be 10 log statements.

On line 12 we call get the global object again so that we can reference the client side function "fillZingerChart" which is called on line 13. Lines 16 - 26 then just validate that the structure built by "fillZingerChart" is basically correct.

Interacting with the UI components

This last test demonstrates how to interact with the visual components of the embedded app.


/**
     * Interact with the UI and run 20 tests confirming under 2 seconds 
     */
    test("run 20 crud tests under 2 seconds", 3, function () {
        stop();
        
        //Navigate to the 'crud' page
        app.childApp('appEmbed').w('actionBtn').base().trigger('vclick');
   
        setTimeout(function() {
            //Confirm crud page
            equal(app.childApp('appEmbed').currentPage().name(),'crud');
            
            //Set UI component to 20 CRUD tests
            app.childApp('appEmbed').setValue('numberOfTestsSlider',20);
            
            setTimeout(function() {  
                //Confirm UI set correctly
                equal(app.childApp('appEmbed').getValue('numberOfTestsSlider'),20, 'slider was set'); 
                
                //Start the test
                app.childApp('appEmbed').w('startTestBtn').base().trigger('vclick'); 
                
                setTimeout(function() {
                    //Get the data from the Zingchart
                    var json = zingchart.exec(app.childApp('appEmbed').w('ZingchartAC').getZingChartId(), "getdata");
                    var title = json.graphset[0].title.text;
                    var tokens = title.split(' ');
                    //Confirm the time is less then 2 seconds
                    ok(tokens[2] < 2000, 'the query is less then 2 seconds');
                    start();
                },4000);
                
            }, 2000);
            
        }, 2000); 
    });
Now the problem we're faced with again is the asyncronous of the calls.  Everytime we interact with a visual component we have to give up control and let the embedded app respond to the interaction.

So in the code above, on line 4 we setup the test and state that there will be 3 assertions.  In the callback we immediately stop the tests so that we can interact with the visual UI components.

On line 8 we trigger a "click" event on the embedded apps actionbtn.

On line 10, we set a standard javascript timeout function for 2000 milliseconds.  When that period of time expires, the function is invoked on line 12.  Here we assert that our new current page in the embedded app is  named 'crud'.

Now on line 15 we set the Slider component to have the value of 20 representing the number of tests to run.

Again we set a timeout to allow the component to respond.  The timeout is also 2 seconds.  In the callback on line 19 we validate that the slider component was indeed set to 20.

Now on line 22 we trigger the "Start Test" button and immediately on line 24 do another timeout.  At the completion of these 4 seconds (line 32),  we can reference the ZingChart that was created.  The ZingChart provides us the JSON data that is then validated on line 30.

Now this is an integration test - I'm using a real database and actually performing the 20 tests.

Summary

As you can see doing unit and integration tests with Application Craft is not so bad.  Other then the asynchronous call the effort is fairly straight forward.

What I really like about this approach is that I continue to do all my work using Application Craft itself.  I didn't have to start writing code using a 3rd party product and then have to worry about where to store that code in GitHub or whatever.  I didn't have to learn yet another tool to do debugging and editing.  With this approach we get to leverage all the power of Application Craft!

Please feel free to leave comments.

No comments:

Post a Comment