homeASCIIcasts

156: Webrat 

(view original Railscast)

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.

Getting Started

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/environments/test.rb file.

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 authentication.

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 username and 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

rake test:integration

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 /login to /sessions/new.

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.

The application’s exception is shown in a browser window.

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 /public/index.html.

Now that we have a default controller we can write the session controller’s create action.

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.

Another Test

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 => "eifion@example.com", :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.