YY Digital Blog

14
February
2013

Alloy Logo

By now readers are already pretty familiar with Alloy and TiShadow. Recently there has been a bit of a discussion about how to test Alloy apps. Aaron Saunder shared a slideshare that runs through testing Alloy apps using behave.js. It is a worthwhile read.

What people might not know is that you can already use TiShadow not only to quickly deploy Alloy apps, but run Jasmine specs simply and without writing a test harness or any modifications to your Alloy app.


Getting Started

We are going to use one of the Alloy test apps included in the Alloy github repository: properties. It can be found in the test/apps/models/properties path.

So your project tree looks something like this:

.
├── app
│   ├── README
│   ├── alloy.js
│   ├── assets
│   │   ├── KS_nav_ui.png
│   │   ├── KS_nav_views.png
│   │   └── ic_menu_add.png
│   ├── config.json
│   ├── controllers
│   │   ├── collection
│   │   │   ├── collectionTab.js
│   │   │   └── row.js
│   │   ├── index.js
│   │   └── modelTab.js
│   ├── models
│   │   ├── collectionTab.js
│   │   └── modelTab.js
│   ├── styles
│   │   ├── app.tss
│   │   ├── collection
│   │   │   ├── collectionTab.tss
│   │   │   └── row.tss
│   │   └── modelTab.tss
│   └── views
│       ├── collection
│       │   ├── collectionTab.xml
│       │   └── row.xml
│       ├── index.xml
│       └── modelTab.xml
├── plugins
│   └── ti.alloy
│       ├── hooks
│       │   └── alloy.js
│       └── plugin.py
├── ti.physicalSizeCategory-android-1.0.zip
└── tiapp.xml

When running the app the modelTab view appears as follow:

modelTab

The view looks like this this:

<Alloy>
  <Tab title="Single Model" icon="/KS_nav_ui.png">
    <Window title="Single Model" layout="vertical">
      <View class="titlebar">
        <Label class="titletext">Single Model</Label>
      </View>
      <Button id="create" onClick="create">create</Button>
      <Button id="destroy" onClick="destroy">destroy</Button>
      <Button id="increment" onClick="increment">Increment</Button>
      <Label id="label">model: {}</Label>
    </Window>
  </Tab>
</Alloy>

And the controller looks like this:

var ID = 'instance';
var app = Alloy.createModel('modelTab'); 

// Change label when 'count' changes on model
app.on('fetch change:count', function(model) {
  $.label.text = 'model: ' + JSON.stringify(app.attributes);
});

// fetch model from Ti.App.Properties adapter
app.set('id', ID);
app.fetch();

////////////////////////////////////
////////// event handlers //////////
////////////////////////////////////
function create(e) { 
  app.save(app.defaults); 
}

function destroy(e) { 
  app.destroy(); 
}

function increment(e) { 
  app.set({
    count: app.get('count')+1,
    id: ID
  }); 
  app.save();
}

We will be testing the controller.


Writing Specs

The TiShadow app has the jasmine library built-in and adds all the boilerplate for you so all you need to do is write your specs.

First create a new directory in your project's root folder called spec. All files in this directory will be picked up by TiShadow. (Note that these files will not be included when you distribute your app as they are not in the app or Resources directory.) Any files ending with _spec.js will be executed when running tishadow spec (see below).

So to test our controller we will write the following spec. The code is pretty self explanatory if you are familiar with Jasmine, but comments have been included to walk you through it:

model_spec.js

 // Create a test suite
 describe("Single Model Test Suite", function() {

   // we need to require alloy so we can access the controllers.
   var Alloy = require("alloy");
   var $;
   beforeEach(function() {
     // create the modelTab controller before executing the
     // "can create and destroy" test suite.
     $ = Alloy.createController("modelTab");
   });
       
   // All UI elements that we will be testing are accessable 
   // via the __view property of the controller.
   describe("can create and destroy", function() {
    // Defined a new test case.
    it("should destroy", function() {
      // Simulate a button click on the destroy button.
      $.__views.destroy.fireEvent("click");
      // A delay is needed for the click event to be fired.
      waits(300);
      runs(function () {
        // Confirm whether the label updates as expected.
        expect($.label.text).toEqual("model: {}");
      });
    });

    // The other tests follow a similar pattern
    it("should create", function() {
      $.__views.create.fireEvent("click");
      waits(500);
      runs(function () {
        expect($.label.text).toEqual('model: {"id":"instance","count":0}');
      });
    });

    it("should increment", function() {
      $.__views.increment.fireEvent("click");
      waits(500);
      runs(function () {
        expect($.label.text).toEqual('model: {"id":"instance","count":1}');
      });
    });

    it("should increment again", function() {
      $.__views.increment.fireEvent("click");
      waits(500);
      runs(function () {
        expect($.label.text).toEqual('model: {"id":"instance","count":2}');
      });
    });


    it("should destroy again", function() {
      $.__views.destroy.fireEvent("click");
      waits(500);
      runs(function () {
        expect($.label.text).toEqual("model: {}");
      });
    });
   });
});

You will note from the spec that we do not need to launch or display the tab in order to test the controller.


Running the tests

Once the spec is complete you can then build your alloy project and execute the tests using the following command:

alloy compile --config platform=ios && tishadow spec

That output will look like this (but with colours):

[TEST] [iphone, 6.1, 192.168.1.9] Runner Started
[TEST] [iphone, 6.1, 192.168.1.9] 
[TEST] [iphone, 6.1, 192.168.1.9] can create and destroy
[TEST] [iphone, 6.1, 192.168.1.9]     √ should destroy
[TEST] [iphone, 6.1, 192.168.1.9]     √ should create
[TEST] [iphone, 6.1, 192.168.1.9]     √ should increment
[TEST] [iphone, 6.1, 192.168.1.9]     √ should increment again
[TEST] [iphone, 6.1, 192.168.1.9]     √ should destroy again
[PASS] [iphone, 6.1, 192.168.1.9] √ 5 test(s) completed.
[TEST] [iphone, 6.1, 192.168.1.9] 
[PASS] [iphone, 6.1, 192.168.1.9] √ 5 spec(s) completed.

We will force a failure by changing line 49 of the spec to

expect($.label.text).toEqual('model: {"id":"instance","count":3}');

This time you just need to type tishadow spec --update as we do not need to recompile the alloy app. The result is:

[TEST] [iphone, 6.1, 192.168.1.9] Runner Started
[TEST] [iphone, 6.1, 192.168.1.9] 
[TEST] [iphone, 6.1, 192.168.1.9] can create and destroy
[TEST] [iphone, 6.1, 192.168.1.9]     √ should destroy
[TEST] [iphone, 6.1, 192.168.1.9]     √ should create
[TEST] [iphone, 6.1, 192.168.1.9]     √ should increment
[FAIL] [iphone, 6.1, 192.168.1.9]     X should increment again
[FAIL] [iphone, 6.1, 192.168.1.9]       => Expected 'model: {"id":"instance","count":2}' to equal 'model: {"id":"instance","count":3}'.
[TEST] [iphone, 6.1, 192.168.1.9]     √ should destroy again
[FAIL] [iphone, 6.1, 192.168.1.9] x 1 of 5 test(s) failed.
[TEST] [iphone, 6.1, 192.168.1.9] 
[FAIL] [iphone, 6.1, 192.168.1.9] x 1 of 5 spec(s) failed.


CI

One more thing people might not be aware of is that tishadow spec has a flag so you can export the test result as JUnit XML report. Enter the following command.

tishadow spec -x 

The following is returned:

[INFO] Beginning Build Process
[INFO] 21 file(s) bundled.
[INFO] BUNDLE sent.
[INFO] [iphone, 6.1, 192.168.1.9] Connected
[INFO] Report Generated: /Users/dbankier/Titanium-Projects/AlloyShadow/build/tishadow/iphone_6_1_192_168_1_9_result.xml

The contents of the file are then as follows:

<?xml version="1.0" encoding="UTF-8" ?>
<testsuites>
<testsuite name="Single Model Test Suite" errors="0" tests="0" failures="0" time="0" timestamp="2013-02-13T14:46:22">
</testsuite>
<testsuite name="Single Model Test Suite.can create and destroy" errors="0" tests="5" failures="0" time="2.346" timestamp="2013-02-13T14:46:20">
  <testcase classname="Single Model Test Suite.can create and destroy" name="should destroy" time="0.321"></testcase>
  <testcase classname="Single Model Test Suite.can create and destroy" name="should create" time="0.506"></testcase>
  <testcase classname="Single Model Test Suite.can create and destroy" name="should increment" time="0.506"></testcase>
  <testcase classname="Single Model Test Suite.can create and destroy" name="should increment again" time="0.507"></testcase>
  <testcase classname="Single Model Test Suite.can create and destroy" name="should destroy again" time="0.506"></testcase>
</testsuite>
</testsuites>


More TiShadow stuff come.

Codestrong!

Updated (4th March, 2013): it should be alloy build --config platform=ios && tishadow spec, i.e. platform=ios not platform=iphone.

 
blog comments powered by Disqus