Agile Web Development

Build it. Launch it. Love it.

Active Test

Contents

Modules and Classes

ActiveTest::Base - wraps Test::Unit::TestCase non-invasively, allows setup/teardown to be nested

ActiveTest::Subject - abstract class for test case types, like Controller and Model.

ActiveTest::Controller - subject for testing controllers, providing dynamic methods and asserts

ActiveTest::Model - subject for testing ActiveRecords, providing dynamic methods and asserts

ActiveTest::Asserts::Assigns - methods for asserting template variable assignments

ActiveTest::Asserts::Difference - methods for asserting difference and change for return values

ActiveTest::Asserts::Validations - methods for asserting ActiveRecord errors

Other

active_test.rb - load up ActiveTest

active_test/filter.rb - the one and only monkey patch: filter out anything in the ActiveTest module

Foreword

I would first like to precede anything with a quick reason why you should test. Testing is, contrary to popular belief, about design. It is not about stamping out bugs. Bugs are the result of bad designs or bad implementations of design. It is in the grey area between the two that makes testing seem like just a mosquito repellent. Tests, like the code they are ‘documenting’ or ‘specifying’ or ‘testing’, are just as prone to bad design or implementation. There are many theories swimming around since the inception of Test Driven Development that try to reconcile this problem of not seeing the wood for the trees.

Rather than come up with some grandiose redefinition of tests, let’s follow the path of least resistance. Why not just make them easier to write? That should help with most cases of bad design or implementation, because then it is faster to recognise a design issue.

About Similarities

While other implementations are attempting, with various success, to extend the functionality of Test::Unit and make it more Rails friendly, they have their own unique ways of setting up and developing tests. The price of their uniqueness is that they move away from the traditional thinking of a Rails developer. They introduce more complications of design and elements to be aware of, meaning more time to understand how something can be done. We should not have to think about the nuances of a test suite and model, how they relate or how to design our application within its specific language. The closer models and tests are to each other in design and usage, the easier it is to design them rapidly. That is why you will see a lot of similarities between ActiveTest and its paronymic relative, ActiveRecord.

There will, of course, be specific elements of ActiveTest you will have to learn if you want to use it, but that’s been kept as basic as possible.

Description

ActiveTest wraps Test::Unit in a Rails-like mould. It does a number of things which would be madness to write for a single project and has caused the author of this plugin considerable madness ‘for the fun of it’. Here are a few of its features:

  • protects you from the disasters of monkey patching
  • fixes issues such as setup/teardown nesting
  • allows inheritance of test cases without hacking Test::Unit, et al.
  • provides a highly extensible, flexible design — forget modifying Test::Unit::TestCase
  • provides class-level macros, a la ActiveRecord, for standard test cases
  • brings design (should do x for y because z) slightly closer to test cases
  • provides instance-level assertions and helper methods
  • provides default specifications for the major forms of testing in Rails
  • follows Convention over Configuration, but allows both
  • generally makes testing pleasurable

If you do nothing more than install ActiveTest and have all your tests inherit from ActiveTest::Base (not recommended, use one of the Subjects of ActiveTest::Subject itself), you will receive a few benefits without drawbacks:

  • setup and teardown are nested
  • all setups/teardowns are executed without super
  • classes inheriting from ActiveTest::Base can be subclassed without detriment
  • anything in the namespace of ActiveTest will not be run by Test::Unit
  • in all other respects behaves exactly like Test::Unit::TestCase

The Design

At the heart of ActiveTest is ActiveTest::Base and the filter addition. The filter prevents anything within the namespace of ActiveTest to be run by Test::Unit::AutoRunner. The Base class itself makes setup and teardown nested, meaning any setup or teardown defined in classes inheriting from Base will be proc’d, stuffed in a stack and executed in the order of definition when setup or teardown are called. This allows all sorts of nifty things to happen seamlessly, such as inheritance.

Base is extended by ActiveTest::Subject, the abstract class for Subjects. Specific sets of ActiveTest::Asserts are mixed into each pre-defined Subject, giving a small set of assertions and convenience methods. Each Subject also has a number of ‘behaviours’ which they can test. These are the macros which can be called through dynamic class methods, such as +succeeds_on :index+.

There is clean distinction between instance and class methods. All class methods on a Subject are behaviours. All instance methods are assertions (e.g. assert_difference), actions (e.g. index_items or find_all), test cases or helping methods.

The class methods are your metalanguage. If you learn the few patterns for each Subject, test-driven design will become a breeze. Read on for more specifics.

Subjects

Subjects are the types of tests you will run, such as Controller or Model. Virgin Subjects can be created by extending the ActiveTest::Subject class itself, exactly how one creates new models inheriting from ActiveRecord::Base. You can, if you wish to have a completely blank slate, inherit directly from ActiveTest::Base too if you do not want behaviours, but you will most likely use the following formulations:

  class ExampleControllerTest < ActiveTest::Controller
  end
  class ExampleTest < ActiveTest::Model
  end

  etc.

The best way to think of subjects is that they are providing models of the Rails domain and ways of specifying their functionality — you already do much of this in the code itself. Since everyone has a controller, view, helper, and record, we can make certain assumptions that, say, ActiveRecord cannot. This is not differing from the ActiveRecord metaphors — it is just filling in some blanks because we can assume a lot with relatively high accuracy. If you don’t like the convenience this provides, just extend ActiveTest::Subject. You’ll get all the fancy stuff of Test::Unit::TestCase and the ability to macro your tests with ‘define_behaviour’. If you don’t even want that, use ActiveTest::Base and you’ll get nothing but nested setup/teardown and the ability to inherit test cases.

Dynamic Methods: Behaviours and Actions

Dynamic methods are at the heart of ActiveTest’s prevention of bad design. They follow rules of simple English and help you think more about the specifications of your design than the programming of a test case. In most cases, it will be the behaviours of your application’s actions that will concern you. If you already think about the actions for each of your objects and how they behave, using Subject dynamic methods will become second nature.

A Subject’s behaviour class method tends to follow this formulation:

  [behaviour] [action], [options]

From this syntax you can see that you test a given behaviour for an action in your application, with an optional indicator that it is an edge case (the options). It’s pretty straight forward.

The behaviour may be, for example, ‘succeeds_on’, ‘fails_on’, ‘assigns_records_on’ or ‘update_record_on’. A behaviour constrains the test case to a predefined set of assertions which will be run for the action you specify.

The action could be, for example, ‘index’ or ‘new’. Actions map directly to the actual methods on your controllers or models.

Assertions

ActiveTest allows you to create ‘assertion packages’. Think ‘Acts As …’. ActiveTest comes with a modest set of assertions. They are enumerated in the contents above. Each of them are pre-included in one or many Subjects for your immediate use. For example, all subjects have ActiveTest::Asserts::Difference, but only ActiveTest::Controller has ActiveTest::Asserts::Assigns. Becoming familiar with the assertions available to you can dramatically speed up your testing.

Inheritance Regained

For a long time, those using Test::Unit have not been able to effectively manipulate inheritance without hacking Test::Unit directly or working around a number of errors caused by inheriting from another test case. The two largest offenders which ActiveTest fixes are, as mentioned, setup/teardown and providing a namespace for ‘abstract tests’, such as the ActiveTest framework itself. We can now (again) do such glorious things as this:

  class ActiveTest::BaseControllerTest < ActiveTest::Controller
    setup
    succeeds_on :index
    succeeds_on :show
  end

  # This inherits setup, index_items and show_items
  class ArticlesControllerTest < ActiveTest::BaseControllerTest
    succeeds_on :new
    succeeds_on :create
    succeeds_on :edit
    succeeds_on :update
  end

Self-Documenting Code

There is a serious concern among developers that making tests more DRY will lose them their ability to document their application through TDD/BDD. As the tests become more terse (so the logic goes) there is less to explain the minutest behaviour of the application. For those who use the agiledox rake task or the equivalent which trawls through the source for standard test method patterns, there is one for ActiveTest too. Unlike its predecessor, it loads up all the tests and libraries and scans each class for the methods which have been created.

The rake task, activetest:agiledox, can be called from the root of your project directory in two ways:

  $ rake activetest:agiledox
    ... checks everything in test/**/*_test.rb
  $ rake activetest:agiledox -- test/unit/articles_test.rb test/unit/pages_test.rb
    ... checks only those files

Example Test Case (Including Commentary)

The following test case is a real example from ActiveTest’s self-tests.

  class ArticlesControllerTest < ActiveTest::Controller

    fixtures :articles

    # Each Subject has a setup class method, some of which take options
    setup

    # Most dynamic methods default to a convention
    succeeds_on :index
    assigns_records_on :index

    succeeds_on :new
    assigns_records_on :new

    # Options may be given.
    succeeds_on :show, :parameters => { :id => 1 }
    assigns_records_on :show, :parameters => { :id => 1 }
    fails_on :show, :parameters => { :id => 19361 }

    # Many options are flexible. Here, you can set parameters to a method or proc which is
    # evaluated in the instance scope (Note: it is defined in the class scope).

    succeeds_on :create, :parameters => :a_good_article
    creates_record_on :create, :parameters => :a_good_article
    fails_on :create, :parameters => proc { a_good_article[:article].merge(:body => nil) }

    succeeds_on :edit, :parameters => proc {{ :id => articles(:nice_article).id }}
    fails_on :edit, :parameters => { :id => 19361 }

    # shortcutting a proc which is reused
    @update_proc = proc {{ :id => articles(:nice_article).id, :article => { :body => "splat" } }}

    succeeds_on :update, :parameters => @update_proc
    updates_record_on :update, :parameters => @update_proc
    fails_on :update, :parameters => { :id => 19361 }
    record_unchanged_on :update, :parameters => { :id => 19361 }

    succeeds_on :destroy, :parameters => proc {{ :id => articles(:nice_article).id }}
    deletes_record_on :destroy, :parameters => proc {{ :id => articles(:nice_article).id }}
    fails_on :destroy, { :id => 19361 }

    succeeds_on :empty_collector
    assigns_empty_on :empty_collector

    protected
    # return a hash for parameters
    def a_good_article
      {:article => { :title => "And now...", :body => "for something completely different" }}
    end
  end

Extending ActiveTest

Now that you’ve seen everything that ActiveTest is about, you’re probably thinking it isn’t enough. Well, half of this library is about making it easy to DRY up tests without losing the power to design and specify. In bringing the concept of modeling to tests, so too come the powers of extension.

Extending ActiveTest is as easy as subclassing and including. It uses the same design as extending ActiveRecord, meaning you can have plugins for custom ActiveTest classes, Subjects or Asserts with a minimum of fuss.

Extending ActiveTest::Base or ActiveTest::Subject

When you want to extend the behaviour of ActiveTest, work on Base or Subject. Base is only for situations which will not be directly inherited for a test case. This is just a matter of design; there is nothing restricting you from doing otherwise. However, any addition to the way tests behave beyond the setup/teardown nesting will most likely be added to Subject, so parallels to ActiveTest::Controller ought to inherit from Subject.

  class ActiveTest::Library < ActiveTest::Base
  end

  class ActiveTest::YourSubject < ActiveTest::Subject
  end
  ActiveTest::YourSubject.class_eval do
    # include the relevant assertions here
  end

Extending ActiveTest::Asserts

If you want to create a new assertion suite for a particular Subject, all you need to do is create a plugin with this init.rb:

  begin
    require 'active_test'
    ActiveTest::PickASubject.class_eval do
      include ActiveTest::Asserts::YourAssertions
    end
    # add more relevant subjects here
  rescue LoadError
    puts "Please install ActiveTest to use this plugin"
  end

And in your plugin/lib:

  module ActiveTest
    module Asserts
      module YourAssertion

        def self.included(base)
          base.extend(ClassMethods)
        end

        module ClassMethods
        end

        def instance_method
        end
      end
    end
  end

That all looks very familiar, doesn’t it?

Vitals

Home http://www.mathewabonyi.com/articles/2006/08/14/activetest-rails-style-testing
Repository http://mabs29.googlecode.com/svn/trunk/plugins/active_test
License Rails' (MIT)
Tags Tag_red
Rating (5 votes)
Owner mabs29
Created 15 August 2006

Comments

Add a comment