Topic: Thoughts on an Acceptance Test DSL...

So as my posts have shown, I've been struggling with finding a comfortable tool/language/whatever to express acceptance tests in. 

I've been thinking about something like:

def create_account_test
  start_at home_page
  click_link "Create Account"
  fill_out_form do |f|
    f.type "SomeUsername", :label=>"Username"
    f.type "email@example.net", :label=>"E-mail Address"
    ...
    f.check "Personal", :label=>"Account Type"
    f.click_button "Next Step"
  end
  fill_out_form do |f|
    f.type "Some", :label=>"Given Name"
    f.type "Username", :label=>"Family Name"
    f.click_button "Create Account"
  end
  find_email (:subject=>"Account Confirmation Required", :to=>"email@example.net") do |e|
    e.assert_text "blah blah blah"
    e.clink_link :containing=>"/account/activate"
  end
  fill_out_form do |f|
    f.type "SomeUsername", :label=>"Username"
    f.type "fooeybarry", :label=>"Password"
    f.click_button "Activate Account"
  end
end

Anyone have any comments on the DSL?  I don't really like have to the "fill_out_form" to introduce a block, but I need to have some place to collect form paramters as they are "entered" and build up the param string to use in the actual post request eventually.  The block for the email, makes sense to me though.  Of course I'm not showing any of the asserts embeded in the custom DSL, but most actions both assert the required pre-condition (such as an element's existence) and then take some action (adding to a parmater list, issueing a get/post/xhr(eventually).

Has anyone else built something similar -- a DSL that is generic and abstracts out the "under the hood details" of knowing the Rails parameter/form naming conventions and the target of links/buttons?

My RoR journey  -- thoughts on learning RoR and lessons learned in applying TDD and agile practices.

Re: Thoughts on an Acceptance Test DSL...

Here's how the DSL evolved, along with the implementation..  I think I'll want to migrate some of my straight regular expressions over to the nested scans that ryanb introduced me to in another thread...  So far I only used it once place, but I do find it leads to clearer code.

The one gotcha that was holding me up for a while here was forgetting to manually populate hidden form paramters into the issued request, but resolving that wasn't too bad...  Now to extract out the DSL to a common base class for all my acceptance tests to use....

If the DSL is useful to anyone else, please feel free to take it and use it however you like.  I haven't had to deal with file uploads or select boxes yet, but it does handle email, link clicking, and form submitting.  Its a little weak on its checkbox/radio button support as it doesn't know how to find the value="foo" from the displayed text.... I don't know of any user interface/accessibility guidelines for knowing how to link the two...  I make something work using labels, but I haven't seen label tags used that way in real life....

require "#{File.dirname(__FILE__)}/../test_helper"

class AccountCreationTest < ActionController::IntegrationTest
  def setup
    @emails = ActionMailer::Base.deliveries
    @emails.clear
  end

  def test_personal_account_creation
    email="test@example.net"
    password="T3st!ng"
    username="TestUser"
    goto_home_page
    click_link "Create Account"
    fill_out_form do |f|
      f.type username, :label=>"Username"
      f.type email, :label=>"E-mail Address"
      f.type password, :label=>"Password"
      f.type password, :label=>"Password Confirmation"
      f.check "Personal", :label=>"Account Type"
      f.click "Next Step"
    end
    fill_out_form do |f|
      f.type "Test", :label=>"First Name"
      f.type "User", :label=>"Last Name"
      f.click "Create Account"
    end
    find_email("Account Pending Confirmation", email) do |e|
      e.contains username
      get e.link("/account/activate")
    end
    fill_out_form do |f|
      f.type username, :label=>"Username"
      f.type password, :label=>"Password"
      f.click "Activate Account"
    end
    verify_link_present "Logout"
  end

  private
  def verify_link_present(text)
    assert response.body.include?(text), "Link with \"#{text}\" not present"
  end

  def fill_out_form
    form = FormSurrogate.new response
    yield form
    form.add_hidden
    post_via_redirect form.url, form.params
  end
 
  def find_email(subject, to)
    email = @emails.find { |e| (e.subject==subject) && (e.to.include?(to))}
    assert_not_nil email, "No matching email found"
    email = EmailSurrogate.new(email)
    yield email
  end
   
  def goto_home_page
    get "/"
    assert_response :success
    assert_template "register/list"   
  end

  def click_link(link_text)
    destination = get_destination_for_link(link_text)
    assert_select "a[href*=#{destination}]", link_text
    get destination
    assert_response :success   
  end

  def get_destination_for_link(link_text)
    regexp = /<a(.*?)href="(.*?)"(.*?)>#{link_text}<\/a>/
    md = regexp.match(response.body)
    md[2]
  end
end

class EmailSurrogate
  include Test::Unit::Assertions
  def initialize(email)
    @email=email
  end
  def contains(text)
    assert_match /#{text}/, email.body
  end
  def link(text)
    regexp = /(https?:\/\/\S*?#{text}\S*)\s/m
    md = regexp.match(email.body)
    md[1]
  end
  def email
    @email
  end
end

class FormSurrogate
  include Test::Unit::Assertions
  def initialize(response)
    @response=response
    @params={}
    @url=""
  end
  def add_hidden
    response.body.scan(/<form.*?action="#{url}".*?>.*?<\/form>/m) do |form|
      form.scan(/<input[^>]+?type="hidden".*?>/) do |hidden_element|
        name = hidden_element.scan(/\bname="(.*?)"/)
        value = hidden_element.scan(/\bvalue="(.*?)"/)
        add_to_params(name.first, value.first) unless name.empty? || value.empty?
      end
    end
  end



  def type(text, label)
    add_to_params(get_name_from_label(label[:label]),text)
  end
  def check(value, label)
    add_to_params get_name_from_label(label[:label]), value
  end
  def click(button_text)
    @url = get_destination_for_button(button_text)
  end
  def get_destination_for_button(button_text)
    regexp = /<form(.*?)action="(.*?)"(.*?)>(.*?)<input(.*?)type="submit"(.*?)value="#{button_text}"(.*?)>/m
    md = regexp.match(response.body)
    assert false, "No button found with button text:\"#{button_text}\"."  if md.nil?
    md[2]
  end
  def get_name_from_label(label)
    regexp = /<label(.*?)for="(.*?)"(.*?)>#{label}<\/label>/
    md = regexp.match(response.body)
    assert false, "No label found with for=\"#{label}\"."  if md.nil?
    regexp = /<input(.*?)id="#{md[2]}(.*?)"(.*?)name="(.*?)"(.*?) \/>/
    md = regexp.match(response.body)
    assert false, "No input found with linked to visual label:\"#{label}\"."  if md.nil?
    md[4]
  end
  def add_to_params(name,value)
    hash=@params
    while name.include?("[") do
      key, name = extract_key_from_name(name)
      hash[key]= {} unless hash.has_key? key
      hash = hash[key]
    end
    hash[name]=value
  end
  def extract_key_from_name(name)
    regexp = /(.*)\[(.*)\]/
    md = regexp.match(name)
    return md[1], md[2]
  end
  def response
    @response
  end
  def params
    @params
  end
  def url
    @url
  end
end

My RoR journey  -- thoughts on learning RoR and lessons learned in applying TDD and agile practices.