Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker

Sencha Touch BDD – Part 5 – Controller Testing

Ken Mayer
Saturday, May 18, 2013

Sencha Touch BDD

tl;dr

A multi-part series of articles on how to test Sencha Touch applications. It uses Jasmine for unit testing and Siesta for integration testing.

Part 5 – Controller Testing

Recap

Part 4 Introduced PhantomJS as an easy and faster alternative to headful Jasmine testing. Part 3 added jasmine-ajax so we can verify that stores and models react properly to back-end data. We also learned how to use stores to test views, without depending on a back-end server. In Part 2 I showed you how to unit test Sencha model classes in Jasmine. In Part 1 I showed you how to set up your Sencha Touch development environment to use the Jasmine JavaScript test framework.

It’s a control thing, but I will let you understand

Sencha Touch controllers usually live within the context of a single application object. Normally, this is handled for you when you invoke Ext.Application() in your app.js file. It creates a singleton object for you in the namespace of your application. For example, if you configured your application’s name to be ‘SenchaBdd’, then the application will be available as the .app attribute of the global SenchaBdd object, that is, SenchaBdd.app.

Unit testing should not have a running application, however. The point is that we are testing classes in isolation. There’s nothing isolated about an integrated, running, Javascript application. There is a relatively simple solution, however; You need to create you own “test” application object that you can then pass as a configuration option when you create controllers under test.

$ cat spec/javascripts/controller/MyControllerSpec.js
describe('SenchaBdd.controller.MyController', function() {
    var controller, app;
    beforeEach(function () {
        app = Ext.create('Ext.app.Application', {name: 'SenchaBdd'});
        controller = Ext.create('SenchaBdd.controller.MyController', { application: app });
        controller.launch();
    });

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

You may want to refactor the application creation and tear-down into a spec helper, to DRY out your tests.

Test behaviors, not events

It’s tempting to write a Jasmine test that tries to trigger an event in the DOM, then follow the event handling through the application. This is the road to hell. If you find yourself trying to simulate an event, please stop. That is what integration tests are better at doing. Controllers are classes like any other, and you should test methods in the same way. For example, let’s drive out a behavior where, when a user taps on the ‘Buy’ button our application sends a request to the back-end.

describe('SenchaBdd.controller.MyController', function () {
  var controller, app;
  beforeEach(function () {
    app = Ext.create('Ext.app.Application', {name: 'SenchaBdd'});
    controller = Ext.create('SenchaBdd.controller.MyController', { application: app });
    controller.launch();
  });

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

  it('#newOrder', function () {
    var order = controller.newOrder();
    expect(order.$className).toEqual('SenchaBdd.model.MyModel');
    expect(order.phantom).toBeTruthy();
  });

  describe('#onBuy', function() {
    it('calls save on the order', function() {
      var myOrder = Ext.create('SenchaBdd.model.MyModel');
      spyOn(myOrder, 'save');
      spyOn(controller, 'newOrder').andCallFake(function() {
        return myOrder;
      });
      
      controller.onBuy()
      
      expect(myOrder.save).toHaveBeenCalled();
    })
  })
});

You might notice that I neither ‘tap’, nor do I test for a ‘POST’ ajax call. The former is better tested through integration tests. The latter is better tested in the model. All the controller need do assert that the model was saved. We trust external classes to function (because they’re tested, too, right?) Testing the #save method on the model follows the same process as testing stores, as I outlined in Part3. Another thing to note is that, under test, this controller does not have any views associated with it; Ext.ComponentQuery calls will return empty (undefined) results. This is to be expected in an isolated test, but may make for some head scratching when you first encounter it. If you must test something in the DOM, you should be writing an integration test anyway.

Ext.define('SenchaBdd.controller.MyController', {
  extend:   'Ext.app.Controller',
  config:   {
    views:   ['MyView'],
    refs:    {
      buyButton: 'myview #buyButton'
    },
    control: {
      buyButton: { tap: 'onBuy' }
    }
  },
  newOrder: function () {
    return Ext.create('SenchaBdd.model.MyModel');
  },
  onBuy:    function () {
    this.newOrder().save();
  }
});

As a personal preference, and make it easier to test and refactor, I have a #newOrder method that delegates to the model to create a new instance.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Sencha Touch BDD – Part 4 – PhantomJS

Ken Mayer
Friday, May 10, 2013

Sencha Touch BDD

tl;dr

A multi-part series of articles on how to test Sencha Touch applications. It uses Jasmine for unit testing and Siesta for integration testing.

Part 4 – Headless testing using PhantomJS

Part 3 added jasmine-ajax so we can verify that stores and models react properly to back-end data. We also learned how to use stores to test views, without depending on a back-end server. In Part 2 I showed you how to unit test Sencha model classes in Jasmine. In Part 1 I showed you how to set up your Sencha Touch development environment to use the Jasmine JavaScript test framework.

I hear it’s all about the cloud these days

Not only do we use Test Driven Development, we test all the time (see also TATFT). In fact, continuous testing is a key component in a well-run agile practice. Pivotal developed ciborg (previously known as Lobot), to automatically deploy Jenkins servers in the cloud. Jasmine has a continuous integration target in its rake tasks, rake jasmine:ci. If you’ve never run it before, now is a good time to try it. You’ll notice that Jasmine launches an instance of the Firefox browser, runs the test suite, then reports the results. That’s great if you’ve got (1) Firefox and (2) a machine that has a display! When you run your CI tests in the cloud, the virtual machines may not be configured with a virtual display. Furthermore, the process is terribly slow.

In this installment, we’ll see how to mix in PhantomJS so we can run our Jasmine tests without the need for a visual browser. PhantomJS is orders of magnitude faster, too.

Do the install dance

  1. Add ‘jasmine-phantom’ to your Gemfile
index e6b5daf..612fb25 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,3 +2,4 @@ source "https://rubygems.org"
 
 gem "rake"
 gem "jasmine"
+gem "jasmine-phantom"

and modify your Rakefile to load the PhantomJS tasks:

--- a/Rakefile
+++ b/Rakefile
@@ -2,6 +2,7 @@
 begin
   require 'jasmine'
   load 'jasmine/tasks/jasmine.rake'
+  load 'jasmine-phantom/tasks.rake'
 rescue LoadError
   task :jasmine do

Finally, run bundle install

Now, when you run rake -T from the command line, you’ll see a new target:

$ rake -T jasmine
rake jasmine             # Run specs via server
rake jasmine:ci          # Run continuous integration tests
rake jasmine:phantom:ci  # Run jasmine specs using phantomjs and report the results

Run rake

Running it will the phantom:ci target will start the Jasmine continuous integration tests in PhantomJS instead of Firefox.

$ rake jasmine:phantom:ci 2>1 /dev/null
[2013-05-06 20:28:15] INFO  WEBrick 1.3.1
[2013-05-06 20:28:15] INFO  ruby 1.9.3 (2012-11-10) [x86_64-darwin12.2.0]
[2013-05-06 20:28:15] INFO  WEBrick::HTTPServer#start: pid=17061 port=60080
Waiting for jasmine server on 60080...

8 specs | 0 failing
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Sencha Touch BDD – Part 3 – Testing Views and Mocking Stores

Ken Mayer
Sunday, May 5, 2013

Sencha Touch BDD

tl;dr

A multi-part series of articles on how to test Sencha Touch applications. It uses Jasmine for unit testing and Siesta for integration testing.

Part 3 – Testing Views and Mocking Stores

In Part 1 I showed you how to set up your Sencha Touch development environment to use the Jasmine JavaScript test framework. In Part 2 I showed you to unit test Sencha model classes in Jasmine.

I don’t normally test views, but when I do

There’s an old MVC mantra: “Fat models, skinny controllers and stupid views.” We don’t want complex logic in our views; that makes them hard to maintain. We don’t want any business logic in our controllers, either. That’s why we have models. Views should be so simple that they don’t require tests. There is a gray area with Sencha, however, where we found testing to be useful in our design process. It has to do with how views interact with stores.

Stores are essentially collections of models. They also encapsulate the persistence layer logic, separate from the business logic of the model. DataViews are a special class of Views in Sencha Touch that will consume a collection to generate a list-type view via a template.

Here are two goals for testing views and stores:

  • Does the view consume the right fields from the store, without hitting the back-end for data?
  • Does the store organize the data from the back-end, i.e. create the interface that is used by the view? Again, without hitting the back-end for data.

Small steps and iterate

Let’s define a very simple view via tests, we’ll make it work using an in-line store, then we’ll refactor the store into its own class. Next, we’ll refactor the storage class to use a remote back-end.

Ext.require('SenchaBdd.view.MyView');

describe('SenchaBdd.view.MyView', function () {
  it("has a list of colors", function () {
    var view = Ext.create('SenchaBdd.view.MyView', {
      renderTo: 'jasmine_content',
      store:    {
        fields: ['color'],
        data:   [
          {color: 'red'},
          {color: 'green'},
          {color: 'blue'}
        ]
      }
    });

    expect(Ext.DomQuery.select('.favorite-color').map(function (el) {
      return el.textContent
    }).join(', ')).toEqual('red, green, blue');

  });
});

Sencha does not come bundled with jQuery, so if you were expecting a DOM query like, $(“.favorite-color”), you might be surprised by the expectation. Ext has its own flavor of querying the DOM, using Ext.DomQuery.select.

Try implementing the view on your own to make the test pass. It should look remarkably similar to this:

Ext.define('SenchaBdd.view.MyView', {
  extend: 'Ext.dataview.DataView',
  xtype:  'myview',
  config: {
    itemTpl: '<div class="favorite-color">{color}</div>'
  }
});

In your application, you probably won’t hardwire a store into the view. In fact, you will probably embed this little view inside a larger container, like so (this adds another tab to the sample app):

--- a/app/view/Main.js
+++ b/app/view/Main.js
@@ -10,6 +10,11 @@ Ext.define('SenchaBdd.view.Main', {
 
         items: [
+            {
+              title: 'Favorites',
+              iconCls: 'star',
+              xtype: 'myview',
+              store: 'mystore',
+              styleHtmlContent: true
+            },
             {
                 title: 'Welcome',
                 iconCls: 'home',

Let’s create a store so our view will show us something:

Ext.define('SenchaBdd.store.MyStore', {
  extend: 'Ext.data.Store',
  config:    {
    storeId: 'mystore',
    fields: ['color'],
    data:   [
      {color: 'red'},
      {color: 'green'},
      {color: 'blue'}
    ]
  }
});

In order to see this view, we’ll need to add it to our app.js file:

--- a/app.js
+++ b/app.js
@@ -31,7 +31,11 @@ Ext.application({
     ],
 
     views: [
-        'Main'
+        'Main', 'MyView'
     ],

+    stores: [
+            'MyStore'
+    ],
 
     icon: {

Now, let’s go back and refactor our test so it uses MyStore instead of one that was hard-wired.

--- a/spec/javascripts/view/MyViewSpec.js
+++ b/spec/javascripts/view/MyViewSpec.js
@@ -2,17 +2,16 @@ Ext.require('SenchaBdd.view.MyView');
 
 describe('SenchaBdd.view.MyView', function () {
   it("has a list of colors", function () {
+    var store = Ext.create('SenchaBdd.store.MyStore', {
+      data:     [
+        {color: 'red'},
+        {color: 'green'},
+        {color: 'blue'}
+      ]
+    });
     var view = Ext.create('SenchaBdd.view.MyView', {
       renderTo: 'jasmine_content',
-      store:    {
-        fields: ['color'],
-        data:   [
-          {color: 'red'},
-          {color: 'green'},
-          {color: 'blue'}
-        ]
-      }
-
+      store:    store
     });
     expect(Ext.DomQuery.select('.favorite-color').map(function (el) {
       return el.textContent

All of our tests should remain green, but we’ve removed the “fake” store from our test.

Let’s get dynamic

Static data stores are boring. The fun starts when you start talking to a back-end API. Let’s say that we have a server that responds to an end point of ‘/colors.json’ with a list of favorite colors. We can even “fake” it by placing a file in the appropriate place. Even so, we don’t want our tests to make network calls to the back-end. That’s not appropriate for unit testing. We’ll use Jasmine’s AJAX mocking helper, jasmine-ajax. At the time of this writing, the 2.0 branch had not been merged into the main line, and we need the 2.0 branch in order to work with Ext.

cd spec/javascripts/helpers
curl -O 'https://raw.github.com/pivotal/jasmine-ajax/2_0/lib/mock-ajax.js'
git add ./mock-ajax.js

And we’ll create our first store spec in spec/javascripts/store/MyStoreSpec.js:

describe('SenchaBdd.store.MyStore', function () {
  var store;
  beforeEach(function () {
    jasmine.Ajax.useMock();
    clearAjaxRequests();
    store = Ext.create('SenchaBdd.store.MyStore')
  });

  it('calls out to the proper url', function () {
    store.load();
    var request = mostRecentAjaxRequest();
    expect(request.url).toEqual('/colors.json');
  });
});

Notice that I call jasmine.Ajax.useMock() and clearAjaxRequests() in the set up block. This is because I want to wait until the very last moment to turn on ajax mocking. The Ext class loader might still be trying to load a class (via xhr), and the mocker will prevent that from happening. I also clear all previous requests (in case there were any left over, to prevent test polution).

When you run Jasmine, you’ll get a test failure, “TypeError: Cannot read property ‘url’ of null” because we haven’t set up the proxy in the store, yet. Let’s do that.

$ cat app/store/MyStore.js
Ext.define('SenchaBdd.store.MyStore', {
  extend: 'Ext.data.Store',
  config: {
    autoLoad: true,
    storeId:  'mystore',
    fields:   ['color'],
    proxy:    {
      type:       'ajax',
      url:        '/colors.json'
    }
  }
});

Now, when you run the test suite, you’ll get a different error! This is because the default settings for the ajax proxy enables caching, paging, etc. Things we don’t need, so we have to turn them off:

    proxy:   {
      type:       'ajax',
      url:        '/colors.json',
      noCache:    false,
      pageParam:  false,
      startParam: false,
      limitParam: false
    }

Now our api test is green! You can even test this in the application by dropping a file, ‘colors.json’ into the public directory.

Let’s add one more test to finish things off.

  it('populates the collection', function () {
    store.load();
    var mockedRequest = mostRecentAjaxRequest();

    mockedRequest.response({
      status:       200,
      responseText: [
        {color: 'red'},
        {color: 'green'},
        {color: 'blue'}
      ]
    });

    expect(store.getCount()).toEqual(3);
    expect(store.getAt(0).get('color')).toEqual('red');
    expect(store.getAt(1).get('color')).toEqual('green');
    expect(store.getAt(2).get('color')).toEqual('blue');
  });

Every mocked ajax request has a response() method that you can use to inject your own response, synchronously, to your tests. This confirms that MyStore properly parses and arranges the received data so that it can be presented by the view.

Validate your mocks

We now have 2 tests that depend on a back-end to respond in a specified way. How do we keep these tests from drifting out of sync? Since we’ve mocked the store in our view test, we might never know if the back-end API changes!

You can wrap Jasmine expectation in functions so that they are reusable. Then we can mix this matcher into our view test to confirm that the store we use there is the same.

Let’s add this function to our SpecHelper.js file:

function myStoreDataIsValid(store) {
  expect(store.getCount()).toEqual(3);
  expect(store.getAt(0).get('color')).toEqual('red');
  expect(store.getAt(1).get('color')).toEqual('green');
  expect(store.getAt(2).get('color')).toEqual('blue');
}

And we’ll replace our existing tests with a single line:

  myStoreDataIsValid(store);

We’ll also modify the MyViewSpec.js:

--- a/spec/javascripts/view/MyViewSpec.js
+++ b/spec/javascripts/view/MyViewSpec.js
@@ -9,6 +9,7 @@ describe('SenchaBdd.view.MyView', function () {
         {color: 'blue'}
       ]
     });
+    myStoreDataIsValid(store);
     var view = Ext.create('SenchaBdd.view.MyView', {
       renderTo: 'jasmine_content',
       store:    store

Similarly, we can refactor our colors array into var:

--- a/spec/javascripts/helpers/SpecHelper.js
+++ b/spec/javascripts/helpers/SpecHelper.js
@@ -16,6 +16,15 @@ afterEach(function () {
     domEl.setAttribute('style', 'display:none;');
 });
 
+var colorsJSON;
+beforeEach(function() {
+  colorsJSON = [
+    {color: 'red'},
+    {color: 'green'},
+    {color: 'blue'}
+  ];
+});

And then use colorsJSON in our tests.

--- a/spec/javascripts/store/MyStoreSpec.js
+++ b/spec/javascripts/store/MyStoreSpec.js
@@ -18,11 +18,7 @@ describe('SenchaBdd.store.MyStore', function () {
 
     request.response({
       status:       200,
-      responseText: [
-        {color: 'red'},
-        {color: 'green'},
-        {color: 'blue'}
-      ]
+      responseText: colorsJSON
     });
     myStoreDataIsValid(store);
   });

and

--- a/spec/javascripts/view/MyViewSpec.js
+++ b/spec/javascripts/view/MyViewSpec.js
@@ -3,12 +3,9 @@ Ext.require('SenchaBdd.view.MyView');
 describe('SenchaBdd.view.MyView', function () {
   it("has a list of colors", function () {
     var store = Ext.create('SenchaBdd.store.MyStore', {
-      data:     [
-        {color: 'red'},
-        {color: 'green'},
-        {color: 'blue'}
-      ]
+      data:     colorsJSON
     });
+    myStoreDataIsValid(store);
     var view = Ext.create('SenchaBdd.view.MyView', {
       renderTo: 'jasmine_content',
       store:    store

What we’ve managed, so far

The toy app is coming along. We’ve test driven a DataView that consumes a back-end API. We’ve added the mock-ajax library so we can unit tests our stores in isolation. We’ve even seen a few techniques for keeping our mocks from getting out of sync (although, if you’ve been paying attention, I’ve still left a gaping hole, that needs to be plugged).

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Sencha Touch BDD Part 2

Ken Mayer
Friday, April 26, 2013

Sencha Touch BDD

tl;dr

A multi-part series of articles on how to test Sencha Touch applications. It uses Jasmine for unit testing and Siesta for integration testing.

Part 2 – Unit Testing Models

In Part 1 I showed you how to set up your Sencha Touch development environment to use the Jasmine JavaScript test framework. We’re going to take a bit of a breather from all the hard work we did last week. In this blog, I’m going to show you how to test simple models.

Let’s have some fun, shall we?

Test-Driven-Development starts with a test, of course. Let’s write one that just asserts that our model class exists:

$ cat spec/javascripts/model/MyModelSpec.js
describe('SenchaBdd.model.MyModel', function() {
  it('exists', function() {
    var model = Ext.create('SenchaBdd.model.MyModel');
    expect(model.$className).toEqual('SenchaBdd.model.MyModel');
  });
});

When we run our tests in the browser, Jasmine reports this:

  Error: [Ext.Loader] Failed loading synchronously via XHR: 'app/model/MyModel.js'; 
  please verify that the file exists. XHR status code: 404

Which is just Ext’s very formal way of saying, “No such class exists” because we haven’t written it, yet.

Let’s write one that makes the test pass:

$ cat app/model/MyModel.js
Ext.define('SenchaBdd.model.MyModel', {
  extend: 'Ext.data.Model'
});

We have proven that we can create a new class, and that it’s name is what we expect.

Attributes

Let’s assert that our model has some attributes:

$ cat spec/javascripts/model/MyModelSpec.js
it('has data', function () {
  var model = Ext.create('SenchaBdd.model.MyModel', {
    name: 'Test',
    email: 'test@example.com',
    favoriteColor: 'blue'
  });
  expect(model.get('name')).toEqual('Test');
  expect(model.get('email')).toEqual('test@example.com');
  expect(model.get('favoriteColor')).toEqual('blue');
});

Which of course, fails, until we add some fields to our model:

$ cat app/model/MyModel.js
Ext.define('SenchaBdd.model.MyModel', {
  extend: 'Ext.data.Model',
  config: {
    fields: [
      { name: 'name', type: 'string' },
      { name: 'email', type: 'string' },
      { name: 'favoriteColor' }
    ]
  }
});

Default values

Let’s say that our model has some default values

$ cat spec/javascripts/model/MyModelSpec.js
it('has default values', function() {
  var model = Ext.create('SenchaBdd.model.MyModel')
  expect(model.get('favoriteColor')).toEqual('yellow');
})

Reload the Jasmine runner in the browser and …

  SenchaBdd.model.MyModel has default values.
  Expected undefined to equal 'yellow'.

Which is easily resolved by adding it to the class:

$ cat app/model/MyModel.js
{ name: 'favoriteColor', defaultValue: 'yellow' }

Validations

One last simple piece, let’s assert that email is a required field.

$ cat spec/javascripts/model/MyModelSpec.js
it('requires an email address', function() {
  var model = Ext.create('SenchaBdd.model.MyModel');
  var errors = model.validate();
  expect(errors.isValid()).toBeFalsy();

  expect(errors.getByField('email')[0].getMessage()).toEqual('must be present');
})

The first expectation asserts that the model is not valid at all. It’s a gatekeeper test. The second test asserts that there’s validation error on the email field (and not some other field).

$ cat app/model/MyModel.js
  config: {
    ...
    validations: [
      { field: 'email', type: 'presence' }
    ]

Roundup

This is a pretty simple set of tests. If you are at all familiar with unit testing, you won’t find much new here. Testing for validations by inspecting on the Errors collection was a little tricky to suss out. Hopefully I’ve saved you a few frustrating moments digging through the source code. What’s also interesting, I think, is how the test report reads:

  SenchaBdd.model.MyModel
    exists
    has data
    has default values
    requires an email address

The test itself communicates something to the reader about the intention of the model. That’s a key concept to understand about TDD; the most important and expensive reader of the application is not the browser, it’s the person who reads and maintains it. It might be your successor or your team mate, or perhaps, yourself, six months from now, when you’ve forgotten everything about this particular patch of code.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Sencha Touch BDD Part 1

Ken Mayer
Wednesday, April 17, 2013

Sencha Touch BDD

tl;dr

A multi-part series of articles on how to test Sencha Touch applications. It uses Jasmine for unit testing and Siesta for integration testing.

Part 1 – Getting Started

In this article you will learn how to set up an application to Jasmine tests in your

Opinionated is a good thing

In my not-so-humble professional opinion, every modern web framework should provide a testing infrastructure with each newly generated application. I’m not concerned if it isn’t my preferred testing package. As long as there’s something. Testing is not an option, and the framework authors probably (hopefully?) test, so why not offer a serving suggestion for new projects? The worst that can happen is that you, the developer, disagree with the choice of framework. There’s a little extra bootstrap cost to replace one framework with another. That’s far less expensive than every new developer discovering a way to test.

Sencha Touch 2.1 has a generator built into its sencha command line tool, but it does not create a test structure as part of the template. This article is the first in a series of discoveries about how to test Sencha Touch applications. I am not claiming that this is the one true way to test. This is not necessarily the best way, either. It is, however, something that works. It installs easily on my development laptop. It gets you to your first passing test quickly. It saves you the cost of exploring all of the options and making these discoveries for yourself. You have plenty of other things to worry about.

But first, you need a web server

Once you’ve installed Sencha Touch and Sencha Command (3.1.0.256 when I wrote this), and you’ve generated the template application, you’ll need to serve the pages locally. Most projects will have some sort of app server already running, but it’s not strictly necessary for testing. When I need to serve pages on my own, I prefer pow. It is a zero-configuration server that can host as many applications as you please. I also like the powder ruby gem; it adds a nice command line interface to manage pow. If you are worried about adding a ruby dependency to your project, stop worrying. Sencha Touch uses the compass ruby gem to generate css files; so you already have a ruby dependency.

pow looks for a rack app, but in my sample app, I don’t have one. pow also looks for a directory named public from which it will server static files. The simplest thing that works is to create a symlink named public that points to the root directory of the project.

# Generate a new ST app
$ cd <touch toolkit directory>
$ sencha generate app senchaBdd ~/workspace/sencha-bdd

# Set up pow/powder
$ cd ~/workspace/sencha-bdd
$ ln -s . public
$ powder up
$ powder link

# Test the server
$ open http://sencha-bdd.dev

If all goes well, you should be able to open the application in any web browser at http://sencha-bdd.dev

Running sample app

Running sample app

Install Jasmine

Installing the stand-alone version of Jasmine will work, but it doesn’t scale to hundreds or thousands of specs. That’s why the Jasmine gem was created. I did some more research and found a way to test using the Jasmine gem.

  1. In the root directory of your project add rake and jasmine to your Gemfile
    $ cat Gemfile
    source "https://rubygems.org"
    
    group :development do
    	gem 'rake'
    	gem 'jasmine'
    end
  2. Run bundle install

  3. Run jasmine init

  4. Jasmine will install a basic set up, but there’s some cruft that you won’t need for a Sencha application.
    rm public/javascripts/Player.js
    rm public/javascripts/Song.js
    rm spec/javascripts/PlayerSpec.js
  5. Edit the src_files entry in spec/javascripts/support/jasmine.yml:
    src_files:
        - touch/sencha-touch-all-debug.js   # Load Sencha library
        - spec/app.js                   # Load our spec Ext.Application
        - app/**/*.js                   # Load source files
  6. Create this file in spec/app.js:
    Ext.Loader.setConfig({
        enabled: true,                  // Turn on Ext.Loader
        disableCaching: false           // Turn OFF cache BUSTING
    });
    
    Ext.Loader.setPath({
        'SenchaBdd': 'app'              // Set the path for all SenchaBdd.* classes
    });
    
    Ext.application({
        name: 'SenchaBdd'               // Create (but don't launch) an application
    });
  7. And this one in spec/javascripts/helpers/SpecHelper.js:
    
    Ext.require('Ext.data.Model');
    
    afterEach(function () {
        Ext.data.Model.cache = {};      // Clear any cached models
    });
    
    var domEl;
    beforeEach(function () {            // Reset the div with a new one.
        domEl = document.createElement('div');
        domEl.setAttribute('id', 'jasmine_content');
        var oldEl = document.getElementById('jasmine_content');
        oldEl.parentNode.replaceChild(domEl, oldEl);
    });
    
    afterEach(function () {             // Make the test runner look pretty
        domEl.setAttribute('style', 'display:none;');
    });

So, what’s going on here? Sencha Touch applications need Ext.Loader to manage class loading. You also need an Ext.Application, especially for controller tests. The modifications to jasmine.yml set up the proper load order, and the jasmine gem will find all of the source files underneath the app/ directory. The app.js is a customized version of your normal app.js that sets up the class loader and global namespace configuration. You should replace “SenchaBdd” with the real name of your application. Two things are happening in SpecHelper.js: First, by default Ext.data.Model caches every model created by the application in a global in-memory array. If you don’t clear it between tests, you can be surprised by test pollution. The second part is to set up and clear a space in the test runner for inserting DOM elements, usually for some sort of view testing.

Create a directory structure that matches your application’s

Your application’s directory structure should look something like this:

├── app
│   ├── controller
│   ├── model
│   ├── profile
│   ├── store
│   └── view

Modify the spec directory so that it mirrors the app/ directory:

├── spec
│   ├── app.js
│   └── javascripts
│       ├── model
│       ├── controller
│       ├── view
│       ├── store
│       ├── profile

Install Jasmine

You can get the stand alone version from http://github.com/pivotal/jasmine. Install it at the in the spec directory. I include the version number, so I can experiment with different versions, but that’s a matter of taste.

├── app
│   ├── controller
│   ├── model
│   ├── profile
│   ├── store
│   └── view
├── public -> .
├── resources/
├── spec/
│   ├── controller
│   ├── jasmine-1.3.1
│   ├── model
│   ├── profile
│   ├── store
│   └── view
└── touch

In order to get Jasmine going, you need a special html file named, by convention, SpecRunner. Add it to spec/ as well. It looks like this:

You will need to modify lines 18 and 22 with the name of your ST app (which can be found in app.json and app.js).

Write one passing test

Create a file, spec/javascripts/sanitySpec.js

describe("Sanity", function() {
  it("succeeds", function() {
    expect(true).toEqual(true);
  });
});


Now load the spec runner into a browser. In the case of this sample app, the url is http://sencha-bdd.dev/spec/SpecRunner.html

Now start the Jasmine spec server from the command line:

    bundle exec rake jasmine

And then open a browser window on http://localhost:8888

You should see the test results with one passing spec.

1 Passing Jasmine Spec

1 Passing Jasmine Spec

If you don’t see this, open up the browser’s developer console to look for clues.

Until next time

That’s it! You now have a complete JavaScript testing framework installed in your application. This is a good time to commit your changes. Celebrate in the glory of the green goodness. You’ve earned it.

Next time, I’ll show you how to test a model class.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

method_missing hazardous to your module?

Ken Mayer
Friday, February 22, 2013

We built an(other) object factory module for our current project and it looks a lot like all the others:

Continue reading →

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

It’s The Volatility That Will Kill You

Ken Mayer
Tuesday, February 19, 2013

Volatility is what Pivotal Tracker uses to measure the consistency of your team’s work output. You can use that number to help you estimate the first approximation to answer the eternal question, “Will I make the deadline?”

One fine day at the office…

The Project

You’ve scoped out 100 points worth of stories for the Next Big Release™. Pivotal Tracker shows your velocity is 10 points per week. Your annual review is in 3 months and on-time delivery of this high profile project will figure prominently.

The Boss Then the CEO walks over to your desk and asks, “Will I make the launch date, 10 weeks from now?”

What do you say?

  1. “Yes, my lord. Of course we’ll make our date! I’m 100% certain of it; Behold; Tracker says we’ll finish in 10 parsecs.”
  2. “Probably; We had some iterations that cleared 30 points, but last week we were working on bugs and only accepted 2 points. A couple weeks of those and we might miss the deadline.”
  3. “There’s no clear answer. There are so many other uncertainties, technical debt, QA, deployment work.”

It’s a trap!

It's A Trap!Hopefully, you answered with “none of the above.” Velocity is just one measure of how your project is performing. Staking your career on it would be foolhardy. The second answer is honest, yet hopelessly vague. The third reply is why many people still think Agile is a way to duck your responsibilities as a software professional. There is hope, however; We can use Pivotal Tracker’s tools to make a better (albeit imperfect) estimate.

Past Performance is No Guarantee of Future Returns, but Yesterday’s Weather is Often Good Enough

Velocity, week over week, varies; sometimes a lot, sometimes a little. It depends on the project. Ideally, each iteration would have the same mix of stories, bugs and chores and Velocity would be very consistent. Steady velocity is a good thing™. In the real world, however, all sorts of things crop up; Your head-count goes up (or down), business priorities shift (or pivot), deferred technical debt demands payment, quality assurance files a slew of bug reports, user testing reveals flaws in product, visual design changes. The real world creates volatility in your Velocity.

A simple measure of this is standard deviation, which Tracker constantly computes for your project. Using that metric, you can decide what you should watch or change in order to meet your goals. Let’s go back to our example and look at the velocity charts in Tracker.

velocity

Assuming that we have a normal distribution of weekly velocities, the first sigma (±35%) will fall into the range of 10±3.5 points each week. That is, there is a 70% probability that your project will deliver all 100 points somewhere between 8 and 16 weeks. Why so much spread? 40% volatility is a big number! In the worst case scenario, where every iteration delivers only 6.5 points, it gets you to your goal in 100 ÷ 6.5 ≅ 16 weeks.

burndown

I Find Your Lack of Faith Disturbing

By now, you’ve had your meeting with the CEO. You’ve shown him the stories left in the backlog, the volatility of the project, and the range of estimates for delivery. This is the beginning of a conversation. If you’re team is not comfortable with the worst case scenario, something must change and, really, you have only two choices; you can reduce volatility or you can reduce scope. You will probably need to do both. Alas, there is no simple formula here. This is where skill, experience and insight will come into play. Here are some suggestions:

Reduce Volatility

  • It’s critical that stories are accepted as soon as possible after they are delivered. Is the project manager unable to accept stores as they are delivered, so they don’t get credited in the iteration where they started? You can backdate acceptance to reflect when the stories were ready (rather than when the PM accepted them), but it is not something I would do on a regular basis.
  • Are the stories marked as bugs and chores *truly* overhead, or are they “stealth” features? Does the story add business value to the product? That’s a feature. Flaws introduced by feature stories are bugs. Design changes surfaced by testing is a new feature.
  • Are there too many stories in flight? Can you deliver stories more reliably by starting fewer at a time? Study after study shows that human beings do not multitask well at all. Do one thing, do it well, then move on.
  • Are there blocked tracks? Do stories get stalled because of dependencies? Can you reorganize your backlog so each story is independent.
  • Are there outside resources, out of your control, that are introducing volatility?
  • Multiple rejected stories are toxic. If your team is getting more than one or two rejects each week, this may be a sign that your stories are not accurately representing what your product manager intended. It’s time to look at your work flow to prevent them from happening so often.
  • Are you not refactoring enough? Constant, steady refactoring, delivered during each story is much better than giant refactors that last a week. You should consider refactoring as critical to your process and not something to do “later, when you have more time for it.”
  • Make all of your projects small by breaking them up. Delivering a project on time is always tricky business. I’ve discovered that it is actually easier to work on projects with short time-lines (6 weeks seems to be a good number). Urgency and a looming dead-line focuses the mind in wonderful ways.
  • As a tactical measure, simplify your pointing strategy. Pivotal Tracker offers many pointing “styles;” linear, quadratic, fibonacci, or you can customize your own. Try going simpler (instead of finer granularity); a 0-1-2-3 scale (easy, medium and hard), might give you a more accurate picture.

Reduce Scope

  • What’s really at risk if you miss the deadline? Often, the perceived urgency is far greater than the actual risk to the project.
  • Are there features that you can jettison?
  • Are there features that you can defer?
  • Are you spending too much time on “pixel-perfect fidelity?” Talk to your designers; look for ways to reduce complexity. One good way to reduce complexity is to lean more heavily on standard user interface libraries (which might affect the unique visual design of the project).
  • Can you make “soft releases” where you deliver fewer features, earlier, to reduce risk?
  • Look at your project goals again. Are the stories in the backlog truly delivering features that will meet your goals?
  • Are there parallel “tracks” that allow you to add man-power to the project (but see below).

Watch for Icebergs

  • Do you need to stand up a new production environment? That will take time. It’s a point-able story. Make sure that all the necessary steps to release are in the budget.
  • Are you refactoring as you go? Have you been postponing technical debt? Those interest payments will start to pile up as you get closer to release time. Make sure you and your team know that keeping the code clean is an essential part of every story.
  • Anything that changes your team will change both Volatility *and* Velocity. Are you adding a new team member? (Remember Brook’s Law, “adding manpower to a late software project makes it later.”) Vacations, holidays, sick days and babies will affect your velocity. Remember to account for it in Tracker.

You’re all clear, kid, now let’s blow this thing and go home!

This article should give you a lot to think about. Good project management is hard work. When projects are just getting started, everything feels fine, and later you start to wonder when everything went to hell. Remember, volatility kills.


Notes

velocity
Just like a speedometer that measures how fast you’re hurtling through space, Tracker’s velocity is a measurement of how fast your team completes stories. Instead of miles or kilometers per hour, Tracker expresses velocity as the number of points completed per iteration (normally a week).Because Tracker stories are assigned point values instead of due dates, Tracker calculates velocity by averaging the number of points you’ve completed over the past few iterations. In Tracker, past predicts future.
volatility
Mathematically, Volatility isStandard Deviation ÷ Mean Velocity
acceptance
If stories languish in the accept/reject state (a field of red and green buttons in the backlog is a strong indicator), several bad things may happen to your project: You lose the fast feedback loop between delivery and deployment. Developers will move on to the next story and may have already lost “context” about past ones. Unaccepted stories can not be deployed, so there’s less and later feedback about the feature in the full project.
stories
What makes a feature or a bug or a chore is worthy of an entire article on its own.
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Deploy strategies for HerokuSan

Ken Mayer
Monday, May 14, 2012

Deploy Strategies

If you look at the network graphs of heroku_san on github, you’ll see a number of branches where the only change is the deletion of the following line from the deploy task:

stage.migrate

If more than a few people are willing to take the effort to fork a gem just so they can delete 1 line, something smells. The reason is that these forkers were using something other than Rails+ActiveRecord+SQL in their project. Some were using Sinatra, others were using Rails, but with CouchDB.

The raison d’être for the heroku_san gem is to make Heroku deploys dirt simple. So, if people are making whole forks to customize the deploy task, we should make it less painful.

Enter strategies

Strategies are an object oriented programming pattern for creating pluggable execution control. Now, there is a new class of objects that inherit from HerokuSan::Deploy::Base. These objects control how deploys are executed for you. The Rails strategy, HerokuSan::Deploy::Rails does exactly what HerokuSan has always done:

  • push to git@heroku.com
  • call rake db:migrate
  • restart

On the other hand, the Sinatra strategy, HerokuSan::Deploy::Sinatra does nothing more than the base strategy:

  • push to git@heroku.com

You can create your own strategies and then configure HerokuSan to use it instead of its default:

Rails 3 projects

Amend your Rakefile:

require 'heroku_san'

class MyStrategy < HerokuSan::Deploy::Base
  def deploy
    super
    # call my own code to do something unique
  end
end

HerokuSan.project = HerokuSan::Project.new(Rails.root.join("config","heroku.yml"), :deploy => MyStrategy)

Sinatra (and other Rack based apps)

Amend your Rakefile

require 'heroku_san'

class MyStrategy < HerokuSan::Deploy::Base
  def deploy
    super
    # call my own code to do something unique
  end
end

config_file = File.join(File.expand_path(File.dirname(__FILE__)), 'config', 'heroku.yml')
HerokuSan.project = HerokuSan::Project.new(config_file, :deploy => MyStrategy)

load "heroku_san/tasks.rb"
  • 0 Shares
  • Share on Facebook
  • Share on Twitter

From customer requirements to releasable gem

Ken Mayer
Sunday, May 13, 2012

One of the many pleasures of working at Pivotal Labs is that we are encouraged to release some of our work as open source. Often during the course of our engagements, we write code that might have wide-spread use. Due to the nature of our contracts, we can not unilaterally release such code. Those rights belong to the client. And rightly so. So, it is an even greater pleasure when one of our clients believes in “giving back” to the community, as well.

One such example is this modest gem, attribute_access_controllable which allows you to set read-only access at the attribute level, on a per-instance basis. For example, let’s say that you have a model Person with an attribute birthday, which, for security purposes, cannot be changed once this attribute is set (except, perhaps, by an administrator with extraordinary privileges). Any future attempts to change this attribute will result in a validation error.

e.g.

> alice = Person.new(:birthday => '12/12/12')
=> #<Person id: nil, attr1: nil, created_at: nil, updated_at: nil, read_only_attributes: nil, birthday: "0012-12-12">
> alice.attr_read_only(:birthday)
=> #<Set: {"birthday"}>
> alice.save!
=> true
> alice.birthday = "2012-12-12"
=> "2012-12-12"
> alice.save!
ActiveRecord::RecordInvalid: Validation failed: Birthday is invalid, Birthday is read_only
> alice.save!(:skip_read_only => true)
=> true

Setting this up is trivial, thanks to a Rails generator which does most of the heavy lifting for you.

rails generate attribute_access Person

After that, you need only know about one new method added to your class:

#attr_read_only(*attributes) # Marks attributes as read-only

There are a few others, but this one, plus the new functionality added to #save and #save! will get you quite far.

And if that’s all that you were looking for when you stumbled across this article, then there’s no need to read any further. Go install the gem and have fun (and may your tests be green when you expect them to be).

From customer requirements to releasable gem

On the other hand, if you are interested in how we got from the original customer story to a releasable open sourced gem, read on. The source code for the module is a mere 34 lines long. It implements 2 new methods, a validator and (gently) overrides #save and #save!. Being good Test Driven Developers, we wrote our specs first, and since we wanted this behavior to be included in several models, we wrote our specs as a shared behavior as well. The spec clocks in at 44 lines, slightly longer than our implementation. All in all, tiny. The whole commit was less than 100 lines of code.

AttributeAccessControllable
  it should behave like it has AttributeAccessControllable
    #attr_read_only(:attribute, ...) marks an attribute as read-only
    #read_only_attribute?(:attribute) returns true when marked read-only
    #read_only_attribute?(:attribute) returns false when not marked read-only (or not marked at all)
    #save! raises error when :attribute is read-only
    #save!(:context => :skip_read_only) is okay
    #save is invalid when :attribute is read-only
    #save(:context => :skip_read_only) is okay

In order to get to something “releasable” we needed a few more things, which we put on our To-Do list:

To do

  1. MIT License
  2. A gem specification
  3. Basic documentation in a README file

The list got longer as we fleshed out both the documentation and the integration tests, as you’ll see in a moment, but first, let’s talk about

Getting the legal issues resolved

Pivotal’s open sourcing policy is straightforward and simple to execute; We don’t touch it. We write code for our clients, it’s their code to do with as they please. My particular client liked the work we did for them and thought it would make a great open source gem. The Director of Engineering signed off on the idea and I paired with him to create the github repository during a lunch break. The first commit was tiny, just a basic directory structure and the existing code. I don’t think the tests passed because they lacked a proper RSpec infrastructure.

Creating the gem

bundler gem DIRECTORY

is your best friend. It set up the layout for us, including an MIT License and a gem specification. It had a boilerplate README, too.

Writing the documentation for the code you wished you had

Next, we wrote a draft of the README file which documented what we knew: You needed a migration to create a column called :read_only_attributes and you needed to include the module into the class. Then we started thinking about the pain points of using our code as is. Wouldn’t it be nice if we could create the migration automatically? Rails generators do that sort of thing, how hard could it be? (Famous last words…) It became clear that we needed to test drive out some new features of the gem that supported the actual module.

To do

  1. MIT License
  2. A gem specification
  3. Basic documentation in a README file
  4. Integration test

I am not a big cucumber fan, but…

Really, I’m not. I used to write Cucumber features all the time, but nowadays, I use a combination of RSpec and Capybara to get most of my day-to-day integration testing done. There is, however, one sweet spot for Cucumber that I’m finding more and more useful; A very high-level document that describes essential features in a way that a reader will say, “Ahhh, so that is how it is supposed to work!” Here’s a copy of the spec I wrote:

Feature: Read only attributes

Scenario: In a simple rails application
  Given a new rails application
  And I generate a new migration for the class "Person"
  And I generate an attribute access migration for the class "Person"
  And I have a test that exercises read-only
  When I run `rake spec`
  Then the output should contain "7 examples, 0 failures"

You probably won’t find any web-steps out there to handle these lines. I use Aruba to handle the dirty work of executing shell commands in a safe sandbox-y way. The step definition file hides most of the ugliness. Even so, most readers could figure out what to do, by hand, for each step.

To do

  1. MIT License
  2. A gem specification
  3. Basic documentation in a README file
  4. Integration test
  5. Generator

Big generators

This gem was my first attempt at writing a generator, so it was awkward. I still don’t understand Thor properly. Fortunately, I happened upon Ammeter, which helped me write out test specs for the generator. If you’ve got good specs, then you can sometimes stumble along until you learn enough to get it right. Alex Rothenberg’s original blog post about the gem was quite informative, as were the test cases from the Devise gem.

I have to admit; constructing the generator was more complex than the original module! There are more “moving parts;” templates, usage files, specs, in addition to the generator itself. So there is a certain amount of overhead that might overwhelm the original content. On the other hand, I learned quite a bit, and the gem is far more useful.

require "spec_helper"
require 'generators/attribute_access/attribute_access_generator'

describe AttributeAccessGenerator do
  before do
    prepare_destination
    Rails::Generators.options[:rails][:orm] = :active_record
  end

  describe "the migration" do
    before { run_generator %w(Person) }
    subject { migration_file('db/migrate/create_people.rb') }
    it { should exist }
    it { should be_a_migration }
    it { should contain 'class CreatePeople < ActiveRecord::Migration' }
    it { should contain 'create_table :people do |t|'}
    it { should contain 't.text :read_only_attributes'}
  end

  describe "the class" do
    before { run_generator %w(Person) }
    subject { file('app/models/person.rb') }
    it { should exist }
    it { should contain 'include AttributeAccessControllable' }
  end

Some interesting things to note; you must require the generator, since it is not pulled in by default. The subject of each suite is a file, not the class AttributeAccessGenerator. The migration_file helper prepends the TIMESTAMP onto the migration file for you. If you need to set up more things for your test, destination_root is a helper with a path to the temporary directory. It remains after the tests have run, which makes it useful when debugging.

Here’s something else that I did not know, but it might help new generator writers; the order in which you define your methods in the generator class is significant. I don’t know how this is done, but each “method” in the generator class is executed in turn. This is important for my generator; the model class definition must exist before I inject the new content that mixes in the module, so I had to write the generate_model method before the inject_attribute_access_content method. I was scratching my head over that one for quite awhile.

require "rails/generators/active_record"

class AttributeAccessGenerator < ActiveRecord::Generators::Base
  source_root File.expand_path('../templates', __FILE__)

  def create_migration_file
    if (behavior == :invoke && model_exists?)
      migration_template "migration.rb", "db/migrate/add_read_only_attributes_to_#{table_name}"
    else
      migration_template "migration_create.rb", "db/migrate/create_#{table_name}"
    end
  end

  def generate_model
    invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
  end

  def inject_attribute_access_content
    class_path = class_name.to_s.split('::')

    indent_depth = class_path.size
    content = "  " * indent_depth + 'include AttributeAccessControllable' + "n"

    inject_into_class(model_path, class_path.last, content)
  end

To do

  1. MIT License
  2. A gem specification
  3. Basic documentation in a README file
  4. Integration test
  5. Generator
  6. Shareable tests

Yo, I hear you like tests in your tests

Lastly, we want to share the testing love. The gem consumer should not have to write tests to drive out the same feature that we have already tested. That would not be very DRY. So, in order to make our shared behavior, er, um, shareable, we moved it into lib with a few wrappers, namely, the spec_support.rb file, which you can include in your own spec files to test drive adding the module to your own classes.

Which is where And I have a test that exercises read-only comes in. You can see this in the steps.rb file:

require 'spec_helper'
require 'attribute_access_controllable/spec_support'

describe Person do
  it_should_behave_like "it has AttributeAccessControllable", :attr1
end

To do

  1. MIT License
  2. A gem specification
  3. Basic documentation in a README file
  4. Integration test
  5. Generator
  6. Shareable tests

Don’t be afraid to release v1.0.0

I am a strong believer in semantic versioning. I simply can not understand why some core ruby tools are still living in version zero land, even after years and years of development and use. So, after a couple of internal commits, we released v1.0.0 of the gem, and less than a day later released v1.1.0 and then v1.1.1! (You probably shouldn’t use anything less than v1.1.1)

An interesting mix

In summary, we used a lot of tools and techniques to go from a simple commit to a shareable gem:

  • Rails generators
  • Cucumber
  • Aruba
  • Ammeter
  • RSpec shared behaviors
  • Integration tests
  • Generator tests
  • Module tests

I encourage everyone to release as much of their work as possible because it raises the state of the art for us all. There are limits, of course, but that still affords lots of wiggle room. Small gems like attribute_access_controllable won’t change the world, but they ease the pain of staying DRY and we all get to learn a little something.

Thanks

To Social Chorus for choosing to open source this code. And to Pivotal Labs for encouraging a better way to do software engineering.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

TDD Action Caching in Rails 3

Ken Mayer
Wednesday, March 28, 2012

On my current project, we needed to prove that an action cache was working as expected. Alas, the blogosphere had either out-of-date or unhelpful information. So, after many experiments, we came up with an RSpec test that does what we want. It seems ugly to me, and I hope there’s a better way. The names have been changed to protect the guilty. Any resemblances to actual classes and methods are purely coincidental.

We needed to confirm that a certain action was cached. This action is preview in the brands controller. Using the usual Rails url helpers, we construct some fixture data.

describe BrandsController do
  describe "caching" do
    let(:brand) { Factory.create(:brand) }
    let(:preview_cache_path) {'views/test.host' + preview_brand_path(brand)}
  end
end

Then we wrote our first test:

it "should action cache #preview" do
  Rails.cache.clear
  get :preview, :brand_id => brand.to_param
  ActionController::Base.cache_store.exist?(preview_cache_path).should be_true
end

This won’t work at all, however; because, in the test environment, caching is turned off.

$ cat config/environments/test.rb
Activator::Application.configure do
...
  config.action_controller.perform_caching = false

So, we need an around block to temporarily turn caching on:

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  example.run
  ActionController::Base.perform_caching = caching
end

That’s great, but the default cache store is the :null store, which, as its name implies, does nothing.

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  store, ActionController::Base.cache_store = ActionController::Base.cache_store, :memory_store
  example.run
  ActionController::Base.cache_store = store
  ActionController::Base.perform_caching = caching
end

Better. But our tests still won’t run because while ActionController uses the cache_store, Observers and Sweepers
use Rails.cache and that is only updated at boot time.

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  store, ActionController::Base.cache_store = ActionController::Base.cache_store, :memory_store
  silence_warnings { Object.const_set "RAILS_CACHE", ActionController::Base.cache_store }

  example.run

  silence_warnings { Object.const_set "RAILS_CACHE", store }
  ActionController::Base.cache_store = store
  ActionController::Base.perform_caching = caching
end

Did I mention that Rails.cache is an accessor for the global, constant, RAILS_CACHE. Ugh.

So, now, we can implement our method

class BrandsController < ApplicationController
caches_action :preview
  def preview
  end
end

But that is still not enough. caches_action
has an interesting performance enhancement; it doesn’t actually set up the action caching unless caching is enabled at class load time. Since we’re not turning caching on until test time, the caches_action method call in the controller class does nothing. We need to re-add it in our test spec.

it "should action cache #preview" do
  Rails.cache.clear
  BrandsController.caches_action :preview # must be recapitulated to get around load time weirdfullness

  get :preview, :brand_id => brand.to_param

  ActionController::Base.cache_store.exist?(preview_cache_path).should be_true
end

This is ugly; it doesn’t test very much (except the underlying caching module, and why bother testing the framework). At least it proves to ourselves that the action is cached and the cache key is what we expect.

Now that we’ve got caching under control, let’s check cache expiration (using a Sweeper).

it "should clear the cache on #update" do
  ActionController::Base.cache_store.write(preview_cache_path, 'CACHED ACTION')

  put :update, id: brand.to_param, brand: {one: 'attribute', after: 'another'}

  ActionController::Base.cache_store.exist?(sign_up_cache_path).should be_false
end

First, I create a cached object, in this case, just the string ‘CACHED ACTION’ and then I invoke the action, and then, I hope, the cache will be expired.

It doesn’t really matter what happens in the #update method of the BrandsController as long as it updates a Brand object. A sweeper in Rails is a mix of Observer & controller filters, so all you need to do is “declare” it in the controller

class BrandsController < ApplicationController
caches_action :preview
cache_sweeper :brand_sweeper
def update
  ...
  @brand.save

Awesome sauce! Now our tests are red and I’m ready to implement the sweeper

class BrandSweeper < ActionController::Caching::Sweeper
  observe Brand # Observers will introspect on the class, but Sweepers don't

  def after_update(brand)
    expire_action :controller => "brand", :action => :preview, :brand_id => brand.to_param
  end
  ...

And voilà! We have greenness.

So what have we learned from this? The Rails source is still your best friend when exploring a sticky problem. Caching is hard, and testing caching is even harder.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Ken Mayer

Ken Mayer
San Francisco

Subscribe to Ken's Feed

Author Topics

bdd (5)
bloggerdome (5)
jasmine (6)
javascript (5)
mobile (6)
sencha (5)
meta programming (1)
refactoring (1)
ruby (1)
agile (18)
planning (1)
design (2)
heroku (3)
rails (8)
workflow (5)
cucumber (3)
testing (4)
performance (3)
devops (1)
jquery (2)
careers (1)
college job fairs (1)
hiring (1)
jobs (1)
recruitment (1)
bundler (3)
facebook (1)
json (1)
nginx (1)
movember (1)
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >