Today I am going to talk about how to get around this and test a form with an ajax autocomplete field. I've built a sample application with all the code examples here and you can download it from http://github.com/alexrothenberg/testing-ajax-example if you like. The application I'm building is just some simple app created with scaffolding that just has a User resource with a name and address. I modified the /users page to not display all users but include the auto_complete typeahead to let you pick a user (imagining there may be a lot) so the page looks something like this.

My first test scenario will ignore the ajax and just test the form which is super easy and can be done by writing a single feature file.
#features/find_a_user.feature
Feature: Allow anyone to find a user and see their details
In order to handle a large set of users
I want search with autocomplete
Scenario: View a candidate detail page without testing ajax
Given "Mickey Mouse" is a user living at "123 Main Street"
When I am on the homepage
And I fill in "Which user" with "Mickey Mouse"
And I press "Find"
Then I should see "Mickey Mouse"
And I should see "123 Main Street"
I run it it all passes and I get
$ rake features
(in /Users/alexrothenberg/ruby/testing-ajax-example)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I "/Library/Ruby/Gems/1.8/gems/cucumber-0.3.2/lib:lib" "/Library/Ruby/Gems/1.8/gems/cucumber-0.3.2/bin/cucumber" --format pretty --require features/step_definitions/user_steps.rb --require features/step_definitions/webrat_steps.rb --require features/support/env.rb --require features/support/paths.rb features/find_a_user.feature
Feature: Allow anyone to find a user and see their details
In order to handle a large set of users
I want search with autocomplete
Scenario: View a candidate detail page without testing ajax # features/find_a_user.feature:5
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I fill in "Which user" with "Mickey Mouse" # features/step_definitions/webrat_steps.rb:22
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
1 scenario (1 passed)
6 steps (6 passed)
I makes use of the default webrat steps that cucumber gives you for free in features/steps/webrat_steps.rb so I don’t even have to write any code to get it to pass but I still haven’t tested any of my code that responds to the autocomplete request. I’d like my test to verify that my routes, controller and model will all work together. So, I write another scenario and run it and this time it fails because I haven't defined the typeahead steps for the typeahead lines.
<first scenario omitted>
Scenario: View a candidate detail page testing (most of) the ajax # features/find_a_user.feature:13
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I typeahead in "Which user" with "Mickey Mouse" # features/find_a_user.feature:16
And I fill in "Which candidate" with the first typeahead result # features/find_a_user.feature:17
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
2 scenarios (1 undefined, 1 passed)
13 steps (3 skipped, 2 undefined, 8 passed)
You can implement step definitions for undefined steps with these snippets:
When /^I typeahead in "([^\"]*)" with "([^\"]*)"$/ do |arg1, arg2|
pending
end
When /^I fill in "([^\"]*)" with the first typeahead result$/ do |arg1|
pending
end
Now I take the hints cucumber has given me and write my autocomplete steps. The interesting thing here is that I need to leave the response object unchanged so I can fill in the form field after running the typeahead step so I can't use the existing webrat steps as they work on a single pair of request and response objects. So I knew I'd be creating a new class with its own request and response that could be used without affecting the one used by my other cucumber steps. Using good outside-in development practices I deferred thinking about how to do that and first wrote my steps file to look something like this. One interesting thing to notice here is that you can invoke a step from inside another step just by omitting the block as I do in the second step.
When /^I typeahead in "(.*)" with "(.*)"$/ do |field, value|
field = field_labeled field
@typeahead = AutoCompleteStepHelper.new(request)
@typeahead.type(field, value)
end
When /^I fill in "(.*)" with the first typeahead result$/ do |field|
When %Q[I fill in "#{field}" with "#{@typeahead.items.first}"]
end
Now when run the feature again it fails telling me I haven’t yet built the AutoCompleteStepHelper.
<first scenario omitted>
Scenario: View a candidate detail page testing (most of) the ajax # features/find_a_user.feature:13
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I typeahead in "Which user" with "Mickey Mouse" # features/step_definitions/autocomplete_steps.rb:1
uninitialized constant AutoCompleteStepHelper (NameError)
./features/step_definitions/autocomplete_steps.rb:3:in `/^I typeahead in "(.*)" with "(.*)"$/'
features/find_a_user.feature:16:in `And I typeahead in "Which user" with "Mickey Mouse"'
And I fill in "Which candidate" with the first typeahead result # features/step_definitions/autocomplete_steps.rb:7
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
2 scenarios (1 failed, 1 passed)
13 steps (1 failed, 4 skipped, 8 passed)
So the next step is to write the AutoCompleteStepHelper class. This class’ job is to have its own request and response that will not affect the ones used by cucumber for the main page requests. It turns out that I can do this by having my class extend ActionController::IntegrationTest and I can even use webrat methods in it because webrat adds its methods to IntegrationTest. In this example I'm calling visit and current_dom and using nokogiri to parse the dom. It is a little weird that I'm subclassing IntegrationTest but this class is not a TestUnit class itself but I decided that was okay.
class AutoCompleteStepsHelper < ActionController::IntegrationTest
def initialize(existing_request)
@controller_name = existing_request.parameters[:controller]
@controller_class = "#{@controller_name.to_s.camelize}Controller".constantize
raise "Can't determine controller class for #{@controller_class_name}" if @controller_class.nil?
@controller = @controller_class.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@response.session = @request.session
end
def type(field, value)
visit url_for(:controller=>@controller_name, :action=>"auto_complete_for_#{field.id}", field.send(:name)=>value)
end
def items
current_dom.search('//ul/li').map(&:inner_html)
end
end
Now when I run the feature it all passes.
<first scenario omitted>
Scenario: View a candidate detail page testing (most of) the ajax # features/find_a_user.feature:13
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I typeahead in "Which user" with "Mickey Mouse" # features/step_definitions/autocomplete_steps.rb:1
And I fill in "Which user" with the first typeahead result # features/step_definitions/autocomplete_steps.rb:7
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
2 scenarios (2 passed)
13 steps (13 passed)
The last step I took was to test that the list returned in the typeahead was correct. I created another scenario in my feature.
Scenario: Typeahead should return 2 users that match but not a third
Given "Mickey Mouse" is a user living at "123 Main Street"
And "Donald Duck" is a user living at "123 Pond Lane"
And "Minnie Mouse" is a user living at "123 Disney Avenue"
When I am on the homepage
And I typeahead in "Which user" with "Mi"
Then I should see in my typeahead "Mickey Mouse"
And I should see in my typeahead "Minnie Mouse"
And I should not see in my typeahead "Donald Duck"
and I added two new steps
Then /^I should see in my typeahead "(.*)"$/ do |text|
@typeahead.response_body.should =~ /#{text}/m
end
Then /^I should not see in my typeahead "(.*)"$/ do |text|
@typeahead.response_body.should_not =~ /#{text}/m
end
Now when I run my features all 3 scenarios are passing
$ rake features
(in /Users/alexrothenberg/ruby/testing-ajax-example)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I "/Library/Ruby/Gems/1.8/gems/cucumber-0.3.2/lib:lib" "/Library/Ruby/Gems/1.8/gems/cucumber-0.3.2/bin/cucumber" --format pretty --require features/step_definitions/autocomplete_steps.rb --require features/step_definitions/user_steps.rb --require features/step_definitions/webrat_steps.rb --require features/support/autocomplete_steps_helper.rb --require features/support/env.rb --require features/support/paths.rb features/find_a_user.feature
Feature: Allow anyone to find a user and see their details
In order to handle a large set of users
I want search with autocomplete
Scenario: View a candidate detail page without testing ajax # features/find_a_user.feature:5
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I fill in "Which user" with "Mickey Mouse" # features/step_definitions/webrat_steps.rb:22
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
Scenario: View a candidate detail page testing (most of) the ajax # features/find_a_user.feature:13
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I typeahead in "Which user" with "Mickey Mouse" # features/step_definitions/autocomplete_steps.rb:1
And I fill in "Which user" with the first typeahead result # features/step_definitions/autocomplete_steps.rb:7
And I press "Find" # features/step_definitions/webrat_steps.rb:14
Then I should see "Mickey Mouse" # features/step_definitions/webrat_steps.rb:93
And I should see "123 Main Street" # features/step_definitions/webrat_steps.rb:93
Scenario: Typeahead should return 2 users that match but not a third # features/find_a_user.feature:22
Given "Mickey Mouse" is a user living at "123 Main Street" # features/step_definitions/user_steps.rb:1
And "Donald Duck" is a user living at "123 Pond Lane" # features/step_definitions/user_steps.rb:1
And "Minnie Mouse" is a user living at "123 Disney Avenue" # features/step_definitions/user_steps.rb:1
When I am on the homepage # features/step_definitions/webrat_steps.rb:6
And I typeahead in "Which user" with "Mi" # features/step_definitions/autocomplete_steps.rb:1
Then I should see in my typeahead "Mickey Mouse" # features/step_definitions/autocomplete_steps.rb:16
And I should see in my typeahead "Minnie Mouse" # features/step_definitions/autocomplete_steps.rb:16
And I should not see in my typeahead "Donald Duck" # features/step_definitions/autocomplete_steps.rb:20
3 scenarios (3 passed)
21 steps (21 passed)
What I've done is not full javascript testing (for that I'm planning to look into Blue-Ridge from Relevance). This technique does allow you to test ajax (skipping the "J") without a browser.
5 comments:
First, thanks a lot for this awesome trick, it's exactly what I needed ...
... But, I'm having a problem using it with an implementation of AuthLogic.
As you can guess, the 2nd request doesn't get the user session infos, so I get redirected to the login page while testing.
Could you help me solving this ?
Thanks again,
Jérémy
Jeremy,
Thanks for your comment, I'm glad you liked this!
I'm not sure about your problem. I did a little googling and found that webrat and rails had some issues with redirects several months ago (see http://www.ruby-forum.com/topic/176981 or https://webrat.lighthouseapp.com/projects/10503/tickets/168-redirects-in-rails-23-not-being-followed-by-webrat). From what I can tell they seem to have been cleared up with webrat 0.4.1. Are you using the latest version?
If upgrading to the latest version doesn't help and you think the problem is related to ajax requests send me some sample code that fails and I may be able to help.
Thanks
Alex
Thanks for your fast answer, and sorry for my late one :)
So, I have WebRat 0.4.4 so it shouldn't be the problem.
Here's the code I want to test : http://pastie.org/500526
And thanks for helping me anyway !
Edit: added < a > tag
Jérémy
The simplest solution I can think of (and what I did in my application) is to hardcode a login right after I create the new @request inside the AutoCompleteStepsHelper class. As long as you can pick a user that is allowed to make the ajax request. I put an example of this on on github that you should be able to customize for your application.
I'm sure there's a more elegant way to tell the new request to use the same session as the original one but had trouble getting that to work.
--Alex
Oh yeah, it works fine now.
Thanks Alex for your time, you have won a new rss suscriber :)
Post a Comment