Call Montréal, Canada 524-949-6869

Blog

Sunday, December 6, 2009

Use Stubs and Expectations to Simplify Testing with Named Scopes

Summary: Use stubs to return the original class for a named scope, and use expectations to specify scope requirements to simplify testing

Testing with chained named scopes that have complex attributes such as conditions can be tricky. I try to combine named scopes with clear names to communicate intent, rather than have long chains floating around in code. This can be done simply by creating a plain old method, with scoped procedures or this hack. This is a good way to keep named scopes DRY when using a specific combination across your application code. Of course you should test your named scopes.

However, consider the situation when adding a simple extension to your named scope that doesn't warrant a new named scope or combination. As an example I have the following code in a catalog application:

class Catalog
  ...
  # Returns a set of ordered page numbers based on the product page numbers
  def page_numbers
    Product.printable.find(:all, :select => "DISTINCT products.page_number").
      collect {|product| product.page_number}
  end
  ...
end

The printable named scope is quite complex and has been tested. I wanted to avoid duplicating test data (although factories would help here). For this method I was only concerned about a collection of products and it's page numbers, not that fact it was scoped. But it is a requirement that the products are scoped to only those that are printable.

Stubbing and setting expectations help us simplify the test and express the requirement. By stubbing the named scope and returning the Product class we can focus on creating test data for which we are concerned -- products with page numbers. The expectation "find printable products", ensures that we have scoped products as required.

class CatalogTest < ActiveSupport::TestCase

  context "with page numbers" do

    setup do
      Product.stubs(:printable).returns(Product)      
    end

    context "generally" do
      setup do
         Catalog.page_numbers
      end

      before_should "find printable products" do
        Product.expects(:printable).returns(Product)
      end      
    end

    context "when no products" do
      should "return an empty array" do
        assert_equal [], Catalog.page_numbers
      end
    end

    context "when multiple products" do
      setup do
        Factory(:catalog_with_page_numbers)
      end

      should "return an array of page numbers" do
         assert_equal [0, 1, 2, 3], Catalog.page_numbers
      end     
    end
  end
end

Although setting this expectation increases coupling to the implementation, we are justified in that it expresses the requirement in the test example.

Developing Ruby on Rails applications and custom Content Management Systems in Montréal. Contact Firsthand.