In the last episode we used Cucumber, a high-level testing framework, to write integration tests for a Rails application. Cucumber allows you to write integration tests in English, then write Ruby code to satisfy each of the English steps in your tests. You might, however, prefer to write integration tests directly in Ruby and if so there is an integration test library called Webrat that you can use to do this.
How Does Webrat Work?
Frameworks such as Selenium or Watir are used to test web applications by simulating a user interacting with a web browser. You write test code to perform tasks such as filling in form fields and clicking links or buttons and then use assertions to ensure that the application is behaving as expected. When you run these tests a browser is opened and you can actually watch the tests taking place. Webrat differs from Selenium or Watir in that it uses a library called Nokogiri to parse the (X)HTML response from a Rails application rather than an actual web browser. This has the advantage of making the tests browser-independent and quicker to run.
We’ll start with a brand-new Rails application. Once we’ve created it we’ll need to configure the test environment to use Webrat. To do this we add this line to the
config.gem "webrat", :version => ">=0.4.4"
Then we use rake to ensure that the gems we need are installed.
sudo rake gems:install RAILS_ENV=test
Once Webrat is installed we’ll need to configure it to work with Rails. Webrat works with several Ruby web frameworks and also with Selenium, so we’ll have to tell it that it’s going to be used in a Rails application. We’ll be using Rails’ built-in integration testing framework, so the configuration goes in
/test/test_helper.rb. We’ll need to add the following code right at the end of the file.
Webrat.configure do |config| config.mode = :rails end
Writing Our First Test
We’re going to be taking a test-driven approach to our application so we’ll start by writing our first integration test. The first part of the application that we’re going to implement is the authentication system so we’ll create an integration test called
script/generate integration_test authentication
The generator will create a file called
authentication_test.rb in the
test directory. In that file we’ll remove the reference to the fixtures, as we won’t be using them, and replace the default test with our first test, which will define the steps required to make a successful login.
require 'test_helper' class AuthenticationTest < ActionController::IntegrationTest test "logging in with valid username and password" do User.create!(:username => "eifion", :password => "secret") visit login_url fill_in "Username", :with => "eifion" fill_in "Password", :with => "secret" click_button "Log in" assert_contain "Logged in successfully." end end
The test above creates a user with
password fields then uses three Webrat methods to visit a page in our application, fill in two text fields and click a button. The test then checks that page contains the text “Logged in successfully.”.
Repeat Until Passing
With our first test written we’ll run it and see what it tells us. Before we run the test we’ll need to run
rake db:migrate to create the schema file. We can then run the integration tests with
As expected the test fails.
1) Error: test_logging_in_with_valid_username_and_password(AuthenticationTest): NameError: uninitialized constant AuthenticationTest::User /test/integration/authentication_test.rb:5:in `test_logging_in_with_valid_username_and_password'
The test fails because we’re creating a
User but we haven’t yet created the
User model. We’ll do that and try again.
script/generate model User username:string password:string
After we’ve run
rake db:migrate again we’ll rerun the test. This time it fails because the
login_url route doesn’t exist.
1) Error: test_logging_in_with_valid_username_and_password(AuthenticationTest): NoMethodError: undefined method `login_url' for #<ActionController::Integration::Session:0x221ca58> /test/integration/authentication_test.rb:6:in `test_logging_in_with_valid_username_and_password'
To fix this we’ll need to create the login page and its define its routes. We’ll create a
sessions controller to handle the logins, and route
script/generate controller sessions new
Then we’ll add the route to
routes.rb, along with the normal RESTful routes for the sessions controller. (The RESTful routes aren’t strictly necessary right now but we’ll need them shortly when we create the form to log in.)
map.resources 'sessions' map.login 'login', :controller => 'sessions', :action => 'new'
That step now passes, but the first
fill_in step fails. Webrat’s
fill_in method looks for a text field either by its name or by the text of its associated label and we haven’t yet created the login form. Our tests look for two text fields with the labels “Username” and “Password” and a button with the text “Log in”, so we’ll create a form in
/app/views/sessions/new.html.erb that will satisfy the tests.
<% form_tag sessions_path do |form| %> <ol class="formList"> <li> <%= label_tag :username, "Username" %> <%= text_field_tag :username %> </li> <li> <%= label_tag :password, "Password" %> <%= text_field_tag :password %> </li> <li> <%= submit_tag "Log in" %> </li> </ol> <% end %>
We run the test again and it now passes as far as the
assert_contains line, which expects the page’s content to contain “Successfully logged in”. This time, not only does the test fail, but the code throws an exception and in these cases a browser window will open and show the error that the application has thrown.
We haven’t written the
create action yet; we’ll do so with an eye on the next line of the test which says that there should be the text “Logged in successfully.” somewhere on the page. We also want to redirect after a successful login, so we’ll have the application redirect to the home page. We don’t yet have a default controller so we’ll create a
home controller with an
index action then add a
map.root line to
routes.rb to define it as the root controller.
map.root :controller => 'home'
As we’ve now defined a root controller we’ll have to remember to remove the default home page in
Now that we have a default controller we can write the session controller’s
def create @user = User.create!(params[:user]) flash[:notice] = "Logged in successfully." redirect_to '/' end
Finally, to put the flash notice on the page we’ll create an application layout file and display it there.
<%= content_tag :p, flash[:notice] unless flash[:notice].nil? %>
We should have fulfilled all of the test’s conditions now and when we run it the test is successful.
Now that the test for a successful login passes we’ll write another one to describe a failing login.
test "logging in with invalid username and password" do User.create!(:username => "eifion", :email => "firstname.lastname@example.org", :password => "secret") visit login_url fill_in "username", :with => "eifion" fill_in "password", :with => "wrong" click_button "Log in" assert_contain "Username or password are incorrect." end
When we run this test it passes the first few steps, but then fails when asserting that the page contains “Username or password are incorrect”. We’ll need to modify the session controller’s
create action to make the test pass.
def create @user = User.find_by_username_and_password(params[:username], params[:password]) if @user flash[:notice] = "Logged in successfully." redirect_to '/' else flash[:notice] = "Username or password are incorrect." render :action => 'new' end end
Both integration tests now pass and so we’re now at the stage where we’d write our third test and repeat the cycle.
Webrat With Selenium
With a few changes Webrat tests can use Selenium so that they run in an actual web browser, rather than with Webrat’s in-memory browser. For more details look at Bryan Helkamp’s blog.