oinopa ponton

I'm Bernerd Schaefer and this is my blog. You can subscribe to this blog and follow me on twitter and github.

Multi-Locale Cucumber Features

So here’s the problem: Your site is available in multiple languages. How do you write features that ensure everthing works regardless of the language?

I can imagine some truly clever solutions to this problem. This isn’t one of them. Instead, this is a practical approach to solving the problem – with the assumption that you’re not using your features to communicate with non-technical stakeholders.

Let’s start with this simple feature describing a user logging in and viewing his profile, and then see how we can transform it to suit our needs:

Feature: User views his profile

  Scenario:
    Given I am logged in as a normal user
    When I go to the home page
    And I follow "Profile"
    Then I should see "Your Profile"

Step 1: Use your translation keys

Okay. While that feature might pass, it’s not going to work if we change the default locale – and it won’t even work if we change our translation keys, so let’s change it up a bit:

    Given I am logged in as a normal user
    When I go to the home page
    And I follow t(profile)
    Then I should see t(your_profile)

Pretty? No. But it should be familiar to anyone who’s used the I18n gem, and it’s super easy to plug into cucumber:

# features/step_definitions/locale_steps.rb

# Allows translation within cucumber features.
#
#   When I follow t(self_generated)
#     => When I follow "#{t(:self_generated)}"
#
Given(
/ ^
  (.*)              # I should see
  (t\(([^)]+)\))    # t(self_generated)
  (.*)              # within "#main"
  $
/x) do |first, _, key, last|
  Given %Q[#{first}"#{t(key)}"#{last}]
end

Step 2: Test multiple locales

Now that we’re using our translation keys, we can enhance our feature to test against multiple locales. We’ll want something like this:

Feature: User views his profile

  Scenario:
    Given my locale is "en"
    And I am logged in as a normal user
    When I go to the home page
    And I follow t(profile)
    Then I should see t(your_profile)

  Scenario:
    Given my locale is "de"
    And I am logged in as a normal user
    When I go to the home page
    And I follow t(profile)
    Then I should see t(your_profile)

This, too, is easy to support. We can add the following step to our locale steps:

# features/step_definitions/locale_steps.rb

Given /^my locale is "([^"]*)"$/ do |locale|
  I18n.locale = locale
end

And then add some cleanup code into our support files, to prevent the locale from one scenario bleeding into the others.

# features/support/locale.rb

After do
  I18n.locale = I18n.default_locale
end

Step 3: Use scenario outlines

We could stop there.. but we’re not done yet! We certainly don’t want to duplicate (or triplicate, or…) all of our scenarios, so let’s use a scenario outline to clean this up:

Feature: User views his profile

  Scenario Outline:
    Given my locale is "<locale>"
    And I am logged in as a normal user
    When I go to the home page
    And I follow t(profile)
    Then I should see t(your_profile)

    Examples:
      | locale |
      | en     |
      | de     |

And there we go! Now we’ve verified that our UI works in both English and German, with just a handful of new step definitions. Happy cuking!

Using Devise? Want a faster test suite?

Here’s the short version: making your password hashes expensive to compute is great for production environments, but not so much for your tests.

And now the longer version.

Inspired by some recent blog posts, I decided to run perftools.rb against my spec suite to diagnose some slowness.

Low and behold, something really strange appeared at the top of the output:

Finished in 109.78 seconds

Total: 12182 samples
    3542 29.1% 29.1%    3542 29.1% BCrypt::Engine.__bc_crypt
    2262  18.6%  47.6%     2262  18.6% garbage_collector
    1590  13.1%  60.7%     2488  20.4% Kernel#require

Hm… 29.1% of CPU time spent inside BCrypt? Wondering where that might be coming from, I started digging around and found this:

Devise.setup do |config|
  config.stretches = 10
  config.encryptor = :bcrypt
end

Ah! According to the documentation for bcrypt-ruby a cost factor of 10 (devise turns stretches into cost factor when using bcrypt) is quite slow. Well, intentionally slow: “If an attacker was using Ruby to check each password, they could check ~140,000 passwords a second with MD5 but only ~450 passwords a second with bcrypt().”

Unfortunately, our test suite is the attacker now: most factories depend on a user, and each new user we create has to generate one of these expensive hashes.

So — what would happen if we replace the bcrypt encryptor with our own encryptor class:

# spec/support/devise.rb
module Devise
  module Encryptors
    class Plain < Base
      class << self
        def digest(password, *args)
          password
        end

        def salt(*args)
          ""
        end
      end
    end
  end
end

Devise.encryptor = :plain

And with that in place, let’s try running out suite again:

Finished in 65.72 seconds

Total: 8428 samples
    2202  26.1%  26.1%     2202  26.1% garbage_collector
    1484  17.6%  43.7%     2329  27.6% Kernel#require
     684   8.1%  51.9%      684   8.1% IO#write

Success! We managed to save 44 seconds by not encrypting user passwords in the test environment! Next step? Digging into all that time in garbage_collector and Kernel#require

An XP Question

I recently finished reading Kent Beck’s Extreme Programming Explained for the first time. It’s really an amazing book, and if you haven’t read it you should do so now.

But I was a bit confused by something, especially thinking about applying XP to a consultancy rather than a team embedded in an organization. Here are two quotes to consider

“As soon as a story is written, try to estimate the development effort necessary to implement it.”

“XP asks programmers to accept responsibility for estimating and completing their own work.”

The question is this. If you are writing and estimating stories at the same time, and writing them each week, how do you determine a client’s budget and time requirements?

I’m conflicted by this. Writing and estimating stories before work has started—or even before you know who will be working on the project—feels more like “requirements gathering” than something belonging to an agile team. And yet it gives you good ground for estimating time-to-launch and budget for a client: “Historically we deliver an average of x points per week, so unless we reduce the scope you’re looking at an approximate launch date of y.”

How have you solved this?

Reply to me on twitter, or shoot me an email. If I get enough responses I’ll put together a followup post.

Laptop Driven Development

Any time someone sees me working on my (1st generation) Macbook Air, I get asked, “Do you do actual development on that?” And the answer is, “Absolutely!” It’s been my primary work / personal machine since I got it. I wanted to share my latest preferred workflow, for anyone else frustrated by developing on a laptop.

For me, the key is getting as much out of my shell, and avoiding all possible context switches. Screen real-estate is sacred – there’s no room for two terminal sessions side-by-side, and even tabs take up precious space.

For those who just want the answer: use \C-z and set up bindings to jump back to where you were.

Setup

For me, the ideal setup requires just two additional lines in ~/.bash_profile:

# ~/.bash_profile
export HISTIGNORE="fg*"
bind '"\C-f": "fg %-\n"'

The first line tells bash to omit any commands that start with fg from the history. This will come in handy later. The second line sets up a readline binding to foreground the process. We’re not using a simple fg or fg %, because we want to be able to swap back and forth between multiple processes, which is exactly what fg %- gets us.

Use Case #1: switch between your editor and a short-lived process.

So you’re writing an integration test with Capybara, and you can’t remember what the api is to visit a particular page (I know, bear with me).

Editing integration test in vim

Normally you might open a new tab, navigate to the project’s directory, and then bundle open capybara.

But we can do better than that! Let’s send C-z (or :stop) to the current process.

Return to the console

Okay. So now we can bundle open capybara, poke around the source, and see, “Oh, duh! It’s ‘visit’!” So we close it up, and then jump back into our code using the binding we set up before (\C-f).

Now were back where we were

And we’re right back where we started!

This technique is also incredibly useful for running specs. \C-z to get back to the console, run the spec, and then \C-f to get back to your spec. If you’ve forgotten what line a failure occured on, just do :! to see the terminal’s history. And since we ignored fg commands, when we want to run the spec again, it’s as simple as \C-z \C-p <enter> (or \C-j if you want to be fancy).

Use Case #2: switch between your editor and another process.

The use case here might be for testing out some changes from an irb or rails console session.

An IRB session

And… what’s that method again? Easy! \C-z and \C-f to hop back to vim.

Back to VIM session

Oh, yeah! \C-z and \C-f and you’re back at your IRB prompt.

Back to the IRB session

And that’s it! I have found this to be a much more pleasant and productive way to work on a laptop than, say, switching between an external editor and the console, or even running commands in a separate tab from vim. And remember, with vim-fugituve, you don’t need to leave vim to commit and push your code!

Introducing Safarium

I recently switched to Chrome after using Safari for almost 3 years, and have been quite happy so far with the results: the memory footprint seems to be much better than Safari, everything feels incredibly snappy, and I’m now completely addicted to the unified location bar. But there were a few things that drove me absolutely crazy, and apparently I was not alone.

<TAB>

In Safari, when you hit <TAB> it moves your cursor’s focus between form elements, making it easy to navigate the page wthout using the mouse. In Chrome (and FireFox), however, <TAB> instead cycles through all clickable elements on the page: links, form elements, etc. When you have been hitting <TAB> for years to, say, skip directly to the search field on Amazon and then discover you need to hit <TAB> 14 times in Chrome to reach the search box, this becomes a killer feature.

<COMMAND-ENTER>

This is another feature of Safari that I used on a regular basis. When you are within a form and hit <COMMAND-ENTER> in Safari, it will be submitted into a new background tab, allowing you to continue working in your active tab. This behavior exists elsewhere in Chrome: <COMMAND-ENTER> in the location bar will open the page in a background tab, and you can even <COMMAND-CLICK> a form’s submit button to open a new tab! But when you <COMMAND-ENTER> from a form element… nothing happens.

Safarium to the rescue!

After just a few days using Chrome, I became frustrated enough that I broke open the Chrome Extensions documentation and put together my own extension: Safarium. Right now it provides the <TAB> and <COMMAND-ENTER> features which I missed so much, and will provide more as seems necessary. It’s not up on Chrome’s extension site, yet, because it needs a logo and some other things, but you can use these intructions for now to get up and running:

First download the extension’s ZIP file. If it wasn’t automatically extracted, make sure to do so now. Next, in Chrome, pull up your extensions list (Window > Extensions). There should be “Developer Mode” link on the right which when clicked will reveal a few buttons. Click ‘Load unpacked extension…’, select the unpacked Safarium extension you downloaded, and that’s it! Happy <TAB>ing!

FakeWeb for the Browser

Before releasing akephalos-0.0.5, I spent some time attempting to resolve some random failures that were showing up in our real integration suites, but not in capybara or akephalos’ specs. Along the way, I realized that because akephalos (and the same goes for selenium) behaves just like a real browser, all of our pages with <script> tags for Facebook Connect, Google Analytics, and Google Maps were actually going out over the network to load the data. Even worse, the Google Maps code was being run on every page—building tiles, placing markers, etc.—which was killing the performace even though no scenarios actually tested the maps. Perhaps not surprisingly, after disabling these resource-intensive external javascripts, the random failures disappeared.

I decided, then, that we needed something like FakeWeb except for resources requested by the browser itself, and after a bit of digging in the HTMLUnit docs, I was able to wire up what I needed to implement filters for akephalos.

Akephalos Filters Configuring filters in akephalos should be familiar to anyone who has used FakeWeb or a similar library. The simplest filter requires only an HTTP method (:get, :post, :put, :delete, :any) and a string or regex to match against.

Akephalos.filter(:get, "http://www.google.com")
Akephalos.filter(:any, %r{^http://(api\.)?twitter\.com/.*$})

By default, all filtered requests will return an empty body with a 200 status code. You can change this by passing additional options to your filter call.

Akephalos.filter(:get, "http://google.com/missing",
  :status => 404, :body => "... <h1>Not Found</h1> ...")

Akephalos.filter(:post, "http://my-api.com/resource.xml",
  :status => 201, :headers => {
    "Content-Type" => "application/xml",
    "Location" => "http://my-api.com/resources/1.xml" },
  :body => {:id => 100}.to_xml)

And that’s really all there is to it! It should be fairly trivial to set up filters for the external resources you need to fake. For reference, however, here’s what we ended up using for our external sources.

Google Analytics

Google Analytics code is passively applied based on HTML comments, so simply returning an empty response body is enough to disable it without errors.

Akephalos.filter(:get, "http://www.google-analytics.com/ga.js",
  :headers => {"Content-Type" => "application/javascript"})

Facebook Connect

When you enable Facebook Connect on your page, the FeatureLoader is requested, and then additional resources are loaded when you call FB_RequireFeatures. We can therefore return an empty function from our filter to disable all Facebook Connect code.

Akephalos.filter(:get, "http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php",
  :headers => {"Content-Type" => "application/javascript"},
  :body => "window.FB_RequireFeatures = function() {};")

Google Maps

Google Maps requires the most extensive amount of API definitions of the three, but these few lines cover everything we’ve encountered so far.

Akephalos.filter(:get, "http://maps.google.com/maps/api/js?sensor=false",
  :headers => {"Content-Type" => "application/javascript"},
  :body => "window.google = {
              maps: {
                LatLng: function(){},
                Map: function(){},
                Marker: function(){},
                MapTypeId: {ROADMAP:1}
              }
            };")