This post is part of a series of reviews on the book Agile Web Development With Rails. Check out the Introduction post for a full table of contents along with some initial notes.

After following all the previous tasks, our depot application is finally done. But finishing the code doesn’t mean it’s finished, it means it’s ready to start the other, let’s say, half of our application development: testing. Remember, the client doesn’t care about the code, he cares that the application is working, and if we just pass this phase, we’ll be handing something to the client that could have glitches, which he will find out sooner or later, and that’s not what we want happening.

We could do testing by hand (filling forms, pressing buttons, etc), which is what alot of people do, and where alot of people get screwed up. An innocent change could lead to a breakdown in some part of the application, which we might have not covered due to the hassle that is testing by hand. That is why we’ll be learning automated tests, which is basically a bunch of classes that simulate the workflow of your application and check to see if everything is running smoothly, and the cool thing is: you’ll only need to do it once, the days of refilling forms over and over again have come to an end!

There are 3 types of tests:

  1. Unit testing – To test out models and check if their relation to the database is flawless and the business rules are being applied correctly.
  2. Functional testing – To test out controllers and check if they’re handling HTTP responses correctly and if they’re displaying the correct view files.
  3. Integration testing – To test out the workflow of the application: if everything is wired together.

Let’s follow the list’s order and start by coding the unit tests. Every time you used a script generating code, Rails would generate its corresponding test file behind the scenes. But before we get to these files we need to fill up our testing database with the same tables and columns as the development database (I’m assuming you already have your test database created, which should be named name_test, where name is the application name you specified when creating the Rails application), and we can do that with a single rake command:

rake test:units

That command copies the database structure and runs all the test files, which should go fine since they all come with a default method that simply checks if true is true, so it should never return errors (it will if you test without copying the database correctly). Now that we have our infrastructure is all set, we can finally start coding our tests.

First we’ll go through the product model, which we created in Task A. Remember all those validation codes? Yes, these:

validates_presence_of :title, :description, :image_url
validates_numericality_of :price
validates_uniqueness_of :title

validates_format_of :image_url,
				  :with => %r{\.(gif|jpg|png)$}i,
				  :message => 'must be a URL for GIF, JPG ' +
				  'or PNG image.(gif|jpg|png)'

validate :price_must_be_at_least_a_cent

protected
  def price_must_be_at_least_a_cent
    errors.add(:price,'should be at least 0.01') if price.nil? || price < 0.01
  end

How can we be sure they really work? Only by testing, and that can be done by the assert method, which simply expects its statement to be true, and if it’s not, the whole testing script will stop and output an error message corresponding to what wasn’t true. Let’s write the full test by adding a method on our product_test.rb file under the unit testing directory:

def test_invalid_with_empty_attributes
  product = Product.new
  assert !product.valid?
  assert product.errors.invalid?(:title)
  assert product.errors.invalid?(:description)
  assert product.errors.invalid?(:price)
  assert product.errors.invalid?(:image_url)
end

We’re creating an empty product and running assertions to it to determine if the validations worked. You can test by typing this in the command prompt:

ruby -I test test/unit/product_test.rb

Sure enought, it worked, the validation kicked in. Now let’s test the price validation (one which determines a price must be positive):

def test_positive_price
  product = Product.new(:title        =>"Title",
                          :description  =>"Description",
                          :image_url        =>"wut.jpg")
  product.price = -1
  assert !product.valid?
  assert_equal "should be at least 0.01", product.errors.on(:price)

  product.price = 0
  assert !product.valid?
  assert_equal "should be at least 0.01", product.errors.on(:price)

  product.price = 1
  assert product.valid?
end

Here we create a dummy object and test its price attribute to 3 different values, comparing the error results to the message we set in the validation method back in the product model. Moving on to the image URL formattig validation:

def test_image_url
  ok = %w{ fred.gif fred.jpg fred.png FRED.JPG FRED.Jpg http://a.b.c/x/y/z/fred.gif}
  bad = %w{ fred.doc fred.gif/more fred.gif.more }

   ok.each do |name|
    product = Product.new(:title        => "My Book Title",
                          :description  => "aaa",
                          :price        => 1,
                          :image_url    => name)
  assert product.valid?, product.errors.full_messages
  end

  bad.each do |name|
    product=Product.new(:title        => "MyBookTitle",
                        :description  => "yyy",
                        :price        => 1,
                        :image_url    =>name)
    assert !product.valid?, "saving#{name}"
  end
end

Here we create 2 arrays, one containing extensions that should pass validations, and the other, not. We then iterate through those arrays create dummy objects and setting their url to the current array element, and assert if its valid or not. Notice the extra parameter added to the assert method which is a trailing message that will get written along case an error pops. To test the last validation method, product uniqueness, we’ll need to add 2 products to the database, of course we could just create 2 test objects, but there’s a better way to do it, and it’s called fixtures.

Long story short, fixtures is a way of pre-determining test data that can be accessed any time and gets injected into the database before any tests are executed, and it’s automatically created when a model is created (by script generators), that means we already have our product fixture, let’s load it up with some data: (products.yml under the fixtures directory):

ruby_book:
  title: Programming Ruby
  description: Dummy description
  price: 1234
  image_url: ruby.png

To tell our unit test file to use that fixture, we need only to add a simple line of code to it:

fixtures :products

Now that we have that concept covered up, let’s finally add a method that will test the product title uniqueness validation:

def test_unique_title
    product=Product.new(:title=>products(:ruby_book).title,
    :description => "yyy",
    :price =>1,
    :image_url => "fred.gif")
    assert !product.save
    assert_equal ActiveRecord::Errors.default_error_messages[:taken], product.errors.on(:title)
   end

Here we try to add a product with the same title as an existing product, which is the fixture named ruby_book, and assert to make sure that the new product wouldn’t get saved into the database. Now that we’re all done with the product model, test again with the rake command and see for yourself that everything is passing the tests so far, which allows us to move on to the cart model, but before we mess with its testing code, let’s add another product fixture:

rails_book:
 title: Agile Web Development with Rails
 description: Dummy description
 price: 2345
 image_url: rails.png

We have our data, let’s move on to the cart testing code, yes, you will have to create that file by hand since we didn’t generate the cart model with a script, just name it cart_test.rb and code this in:

require 'test_helper'
class CartTest<ActiveSupport::TestCase

  fixtures:products
  
  def test_add_unique_products
    cart = Cart.new
    rails_book = products(:rails_book)
    ruby_book = products(:ruby_book)
    cart.add_product rails_book
    cart.add_product ruby_book
    assert_equal 2,cart.items.size
    assert_equal rails_book.price+ruby_book.price, cart.total_price
  end
end

We’re basically adding two products and making sure that they have been added sucessifully and that the total_price method is doing its job right. Moving on to the next test method:

def test_add_duplicate_product
  cart = Cart.new
  rails_book = products(:rails_book)
  cart.add_product rails_book
  cart.add_product rails_book
  assert_equal 2*rails_book.price, cart.total_price
  assert_equal 1, cart.items.size
  assert_equal 2, cart.items[0].quantity
end

Now we’re testing if an item quantity gets bumped case the same product gets added to the cart. Notice we’re having some pretty ugly duplication of code here, we’ve declared twice the cart variable and the rails_book variable, and we could as well put them (along with the ruby_book variable) into an instance variable that could be used by all testing methods. This can be done by the setup test method, which creates an environment for our testing, supplying us with instance variables that we can use throughout the test methods, so now our cart testing class should look like this:

require 'test_helper'

class CartTest<ActiveSupport::TestCase

  fixtures:products

  def setup
    @car = Cart.new
    @rails = products(:rails_book)
    @ruby = products(:ruby_book)
  end

  def test_add_unique_products
    @cart.add_product @rails
    @cart.add_product @ruby
    assert_equal 2, @cart.items.size
    assert_equal @rails.price + @ruby.price, @cart.total_price
  end

  def test_add_duplicate_product
    @cart.add_product @rails
    @cart.add_product @rails
    assert_equal 2 * @rails.price, @cart.total_price
    assert_equal 1, @cart.items.size
    assert_equal 2, @cart.items[0].quantity
  end
  
end

Run the rake testing command again and you’ll see that everything works out, as it should (I wouldn’t guide you with flawed code :P). Time to move on to the functional tests, the ones responsable for dealing with controllers. As said earlier, controllers are the ones responsible for dealing with requests and responses that get viewed, and for this, a functional test class has by default 3 instance variables: @controller which is an instance of the controller being dealt with, @request which deals with the HTTP request details, and @response, responsible for responding HTTP requests returning templates so the browser can then render them.

Since this got a little more complicated, let’s take it slow, we’ll start by adding a simple index method to the admin_controller_test file:

def test_index
  get:index
  assert_redirected_to :action => "login"
  assert_equal "Please login", flash[:notice]
 end

Get method simulates an HTTP GET request to the index action, and since we know that a non-logged user gets redirected to the login controller, we have to make sure that happens. So, we need a user to actually login, and for that we’ll use fixtures, but there’s a small problem: the password gets hashed. That’s where we introduce the dynamic fixtures concept: (users.yml file)

<% SALT = "NaCl" unless defined?(SALT) %>

dave:
name: dave
salt: <%= SALT %>
hashed_password: <%= User.encrypted_password('secret',SALT) %>

Yes, Rails supports the same syntax used to insert dynamic values in templates on test files. Now we can test with effectiveness since the user’s password is being hashed. On with a new test method:

require 'test_helper'

class AdminControllerTest < ActionController::TestCase

  fixtures :users
  
 def test_index
  get:index
  assert_redirected_to :action => "login"
  assert_equal "Please login", flash[:notice]
 end

 def test_index_with_user
  get:index, {}, {:user_id=>users(:dave).id}
  assert_response :success
  assert_template "index"
 end
end

Notice the new parameters in the get method, the second one is a hash with parameters to be passed to the action and the third is to set session variables. The test should use our fixture user ‘dave’, respond correctly and render the admin index file. In this method we’re forcing a user by setting the session variable, but we still need to check the login feature, which we will be doing now:

def test_login
  dave = users(:dave)
  post :login, :name => dave.name, :password=> 'secret'
  assert_redirected_to :action => "index"
  assert_equal dave.id, session[:user_id]
end

By using post, we’re telling rails that we want to pass dave’s name and password to the login controller. But what if we enter a bad password?

def test_bad_password
  dave = users(:dave)
  post :login, :name => dave.name, :password=> 'wrong'
  assert_template "login"
end

Bad password means getting redirected back to the login page. Run the rake testing command to see that we’re all good to move on to the next level of testing which is exercising the flow of the application. One way to do this is simply getting a story and coding it up, for example:

A user goes to the index page. They select a product, adding it to their cart, and checkout, filling in their details on the checkout form. When they submit, an order is created containing their information, along with a single line item corresponding to the product they added to their cart.

Since we made that up, we’ll have to generate the files needed for it:

ruby script/generate integration_test user_stories

With our files ready, it’s time to translate our little story into one big chunk of code:

require 'test_helper'
class UserStoriesTest < ActionController::IntegrationTest

  fixtures :products

  #A user goes to the index page. They select a product, adding it to their
  #cart, and checkout, filling in their details on the checkout form. When
  #they submit, an order is created containing their information, along with a
  #single line item corresponding to the product they added to their cart.

  def test_buying_a_product
    LineItem.delete_all
    Order.delete_all
    ruby_book = products(:ruby_book)
    get "/store/index"
    assert_response :success
    assert_template "index"
    xml_http_request :put, "/store/add_to_cart", :id=>ruby_book.id
    assert_response :success
    cart = session[:cart]
    assert_equal 1, cart.items.size
    assert_equal ruby_book, cart.items[s].product
    post "/store/checkout"
    assert_response :success
    assert_template "checkout"
    post_via_redirect "/store/save_order", :order =>{ :name => "Dave Thomas", :address => "123 The Street", :email => "dave@pragprog.com", :pay_type => "check" }
    assert_response :success
    assert_template "index"
    assert_equal 0, session[:cart].items.size
    orders = Order.find(:all)
    assert_equal 1,orders.size
    order = orders[0]
    assert_equal "Dave Thomas", order.name
    assert_equal "123 The Street", order.address
    assert_equal "dave@pragprog.com", order.email
    assert_equal "check", order.pay_type
    assert_equal 1, order.line_items.size
    line_item = order.line_items[0]
    assert_equal ruby_book, line_item.product
  end
end

Try reading for yourself what each line of code does, I’ll just explain about a few methods that we haven’t used yet. The get method used here is different from the one used in the functional methods because here the scope is global, so we pass a link with a controller and an action, instead of just an action. The xml_http_request refers to the AJAX call that happens when you add something to the cart, in this case, the ruby_book id. Post_via_redirect sends some HTTP POST information (contained in the hash) and automatically redirects to the page in the first parameter.

That’s it, we’ve covered the 3 types of testing, of course we didn’t teste everything that could be tested, but just enough for it to be clear so you can create your own tests. Thanks to whoever read all the tasks and I hope you learned something from them! Feel free to post any questions.