Sunday, October 19, 2008

Cucumber scenarios need a title

So I'm playing with Behavior Driven Development (BDD) and Ruby on Rails using Cucumber and Webrat. I thought I'd start with the simplest feature I could think of, "I want to see the application name in the title". So, off we go with features/site_layout.feature:
Feature: Site Layout
  In order to build familiarity
  As a user
  I want to see the application name in the title

  Scenario:
    Given I am on the home page
    Then the title tag should be "WOW App"
I create features/steps/site_steps.rb:
Given /^I am on (.*)$/ do |page|
  visits case page
         when "the home page"
           "/"
         else
           raise "Can't find mapping from \"#{page}\" to a path"
         end
end

Then /^the (.*) tag should be "(.*)"$/ do |tag, content|
  response.should have_tag(tag, content)
end
And run it with "rake features". What I get is:
C:\dev\temp>rake features
(in C:/dev/temp)
Feature: Site Layout  # features/site_layout.feature
  In order to build familiarity
  As a user
  I want to see the application name in the title
  Scenario: Given I am on the home page     # features/site_layout.feature:6
    Then the title tag should be "WOW App"  # features/steps/site_steps.rb:10
      You have a nil object when you didn't expect it!
      The error occurred while evaluating nil.content_type (NoMethodError)
      ...
      ./features/steps/site_steps.rb:11:in `Then /^the (.*) tag should be "(.*)"$/'
      features/site_layout.feature:8:in `Then the title tag should be "WOW App"'


1 steps failed
rake aborted!
Not quite what I had in mind, I was expecting a failure, but got an error. Nevermind, this is all very new to me, so I'll push on and it'll all become clear right? I create an app/views/layouts/application.html.erb:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>WOW App</title>
  </head>
  <body>
  </body>
</html>
Run the feature again ... no joy, same error. Hours of web searching leads me nowhere. Finally while looking at some examples I noticed that my Scenario doesn't have a description, so I add one on line 6:
Feature: Site Layout
  In order to build familiarity
  As a user
  I want to see the application name in the title

  Scenario: On the home page
    Given I am on the home page
    Then the title tag should be "WOW App"
Now I get:
C:\dev\temp>rake features
(in C:/dev/temp)
Feature: Site Layout  # features/site_layout.feature
  In order to build familiarity
  As a user
  I want to see the application name in the title
  Scenario: On the home page                # features/site_layout.feature:6
    Given I am on the home page             # features/steps/site_steps.rb:1
      No route matches "/" with {:method=>:get} (ActionController::RoutingError)
...
      features/site_layout.feature:7:in `Given I am on the home page'
    Then the title tag should be "WOW App"  # features/steps/site_steps.rb:10


1 steps failed
1 steps skipped
rake aborted!
A quick addition to config/routes.rb:
map.root :controller => 'example'
And a stub controller from "ruby script\generate controller Example index", I get:
C:\dev\temp>rake features
(in C:/dev/temp)
Feature: Site Layout  # features/site_layout.feature
  In order to build familiarity
  As a user
  I want to see the application name in the title
  Scenario: On the home page                # features/site_layout.feature:6
    Given I am on the home page             # features/steps/site_steps.rb:1
    Then the title tag should be "WOW App"  # features/steps/site_steps.rb:10


2 steps passed
Hurrah!

Sunday, October 5, 2008

Where to put sqlite3.dll on Windows

When I first started using SQLite3 on Windows, most people suggested downloading sqlite3.dll from www.sqlite.org and storing it in ruby/bin so Windows could find it. For a while, this strategy worked fine but I really didn't like the application requiring users to put that DLL into an acceptable place. I don't know why it didn't occur to me earlier, but, given that this is only intended to be a Windows application, why not just modify the PATH environment variable within the application. So that's what I've done. In config/boot.rb:
# Make sqlite3.dll available
ENV['PATH'] += ";#{RAILS_ROOT}/vendor/bin/sqlite3"

Thursday, October 2, 2008

class << self

I've come across this construct many times while browsing a number of open source projects:
class MyClass
  class << self
    def a_class_method
      ...
    end
  end

  def an_instance_method
    ...
  end
end
I finally got it today ... there's no black magic ... all "class << self" means is:
add the following class method definitions to the class MyClass.
Or,
open the class MyClass, and add the following class methods.
Now that I'm in the gang and understand this incantation, I'm not sure I like it. It seems like a nice way to group all your class methods together, but if you define several class methods then it becomes harder to recognize whether a method belongs to the class or an instance. I think it's cleaner to explicitly state the ownership of each method:
class MyClass
  def self.a_class_method
    ...
  end

  def an_instance_method
    ...
  end
end

Saturday, September 20, 2008

My first Rails bug

UPDATE January 14, 2009 -> The patch for this bug was accepted!

I'm pretty new to Rails and my first project included uploading videos. Shouldn't be too difficult I thought, after a little Google searching, I came up with the perfect example by Jim Neath: Converting Videos with Rails: Converting the Video Wanting to practice new skills with RSpec and Cucumber I wrote my first feature spec:

Feature: Upload videos
  In order to provide videos to users after hours
  As a videographer
  I want to upload videos

  Scenario: A valid filename is provided
    Given I go to the new video page
    And I browse to the file "Movie_0001.avi"

    When I submit the upload

    Then I should see "success"
    And the file should be uploaded
and the supporting steps file, upload_steps.rb:
require 'ftools'
require 'mime/types'

When /I browse to the file \"(.+)\"/ do |path|
  @original_filepath = File.join('features/fixtures/', path)
  mime_types = MIME::Types.of(@original_filepath)

  attach_file 'video[source]', @original_filepath, mime_types[0].content_type
end

When 'I submit the upload' do
  click_button 'Create'
end

def uploaded_filepath
  uploaded_basename = File.basename(@original_filepath)
  File.join(RAILS_ROOT, "public/videos/1", uploaded_basename)
end

Then /the file should be uploaded/ do
  assert File.compare(@original_filepath, uploaded_filepath)
end
what I got was:
...
    And the file should be uploaded
       is not true. (Test::Unit::AssertionFailedError)
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:48:in `assert_block'
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:500:in `_wrap_assertion'
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:46:in `assert_block'
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:63:in `assert'
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:495:in `_wrap_assertion'
      c:/ruby/lib/ruby/1.8/test/unit/assertions.rb:61:in `assert'
      ./features/upload/steps/upload_steps.rb:22:in `And /the file should be uploaded/'
      features/upload/upload.feature:14:in `And the file should be uploaded'
...
On closer inspection, the test was failing because the uploaded file was truncated in some bizarre way. However, if I ran the application and manually upload a file from the browser, everything worked fine. After much hunting I ended up in rails/actionpack/lib/action_controller/integration.rb where in multipart_body, the mode is not specified in the call to File.open, so it defaults to "r". This is all well and good on anything but Windows, which I happen to be using! Windows requires that the mode be specified as "rb" to ensure the file is read as binary. I submitted a patch but I'm not holding my breath for it to be pulled in anytime soon.

Ruby PCAN DLL Wrapper

Inspired by System Testing In Ruby (Systir), I have published a small Ruby PCAN USB DLL Wrapper. Now with a small device available for less than $300 and some free software, I can write automated system test scripts of the form:
send tftp_rrq("autoexec.bat").with(blksize(2036)) verify_target_sends oack(blksize(2036) send ack(0) verify_target_sends autoexec_bat send ack(1)
If you have rubygems installed, PCAN DLL Wrapper is easily obtained:
set http_proxy=http://my_proxy_host.com:80 gem install pcanusb
and then use:
require 'pcan_usb' PCAN_USB.init(PCAN_USB::BAUD_1M) PCAN_USB.write(0x0E100501, "Hello World!") PCAN_USB.close

Collection o quotes

Ricky Hustler, a friend of mine, used to use this one on a regular basis:
"You never see a luggage rack on a hearse"
I came across this on James Grenning's blog:
"It's easier to act your way into thinking differently than to think your way into acting differently"
From a friend's coffee cup:
"Lead, follow, or get out of my way"
Susan Jeffers:
"Feel the fear and do it anyway"

Tuesday, June 10, 2008

ActiveRecord requires RAILS_ROOT for relative Sqlite path

I'm writing a little time tracking tool for Windows in Ruby. The data is stored in a database so I figured I'd use ActiveRecord and maybe learn something about Rails along the way. Everything I found about ActiveRecord tells me that it "can be used independently outside of Rails". One minor detail that I just figured out:
If you want to use a relative path for a sqlite3 database in your database.yml, you have to define RAILS_ROOT.
For example, if config/database.yml =>
production:
  adapter: sqlite3
  database: db/production.sqlite
ActiveRecord initialization (mine's in config/boot.rb) looks like:
RAILS_ROOT = "#{File.dirname(File.expand_path(__FILE__))}/.."
RAILS_ENV  = ENV['RAILS_ENV'] || 'production'

$LOAD_PATH.unshift "#{RAILS_ROOT}/vendor/sqlite3"
config = YAML::load(IO.read("#{RAILS_ROOT}/config/database.yml"))
ActiveRecord::Base.configurations = config
ActiveRecord::Base.establish_connection(config[RAILS_ENV])