ActiveTest: Examination, Part IV: Salvaging Useful Ideas
Posted by Mathew Abonyi Tue, 02 Jan 2007 04:13:42 GMT
DRY and Abstraction: There is a difference between practices of DRY and Abstraction. When you are being DRY, you are either addressing a new repetition or continuing an earlier practice. The primary focus of being DRY is not to create higher level tools for future use. It is a practice of improving past and present code. It replaces, deletes, compresses, encapsulates—it is passive redesign. Abstraction, on the other hand, is active design. When you try to be DRY without a real model, you are actually abstracting. The process of abstraction is a wider programming activity which can lead quite naturally into over-design, because without real models and implementation, it often fails to address real issues.
With that short sermon, let’s make ActiveTest useful again. As I mentioned in the introduction to this mini-series, there are things to take away from the old version of ActiveTest. We’ll now look at them in full and show why they are still useful.
1. Test::Unit inheritance: filtering classes
One of the few patches in the old ActiveTest is to modify Test::Unit’s very well hidden collector filter. The idea of filtration is critical to making inheritance possible. If you have a situation where many ActiveRecord models share many attributes, for example in a Single Table Inheritance for a ‘Content’ model, and they all have a body, title, excerpt, owner, created_at, created_by, updated_at and updated_by, the tests for these attributes will be identical across all model tests. So why not create a ‘ContentTestCase’ suite which each model test inherits? With Test::Unit you can make a ‘StandardContentTests’ module and mix it in, but then you are looking for a place to put them and later looking around for those abstracted tests. Alternatively, if you want to use inheritance, those tests will be run immediately. You want to filter out the abstract class, but still be able to inherit from it. By modifying Test::Unit’s collector filter, it is possible to put anything in the ActiveTest namespace and it will not be run:
require 'test/unit/autorunner'
class Test::Unit::AutoRunner
def initialize_with_scrub(standalone)
initialize_without_scrub(standalone)
@filters << proc { |t| /^ActiveTest::/ =~ t.class.name ? false : true }
end
alias_method :initialize_without_scrub, :initialize
alias_method :initialize, :initialize_with_scrub
endI won’t go on about the peculiarity of having the filter in AutoRunner, but the inaccessibility of the filter requires a monkey patch like the one above. Just poking into initialize lets us add to the @filters instance variance on AutoRunner and tell it to ignore the ActiveTest namespace. This technique is more about just reading through the code and finding the right place to patch.
2. Metaprogramming techniques (not all of them)
Wrapping define_method has proven to be pretty pointless other than ensuring an unique method name, but developing a lower-level language to standardise class-level macros is a general idea which ActiveTest could keep with some minor adjustments. Ignoring what define_behavioral does, this is a very short macro definition (from ActiveTest::Controller):
def assigns_records_on(action, options = {})
define_behavioral(:assign_records_on, action, options) do
send("call_#{action}_action", options)
assert_assigned((options[:with] || plural_or_singular(action)).to_sym)
end
endIf define_behavioral is made to register behaviors for a better parsing method than using method_missing, then we give it some meaning, but not really enough to keep it. The rest of the method definition, however, is quite clean for creating macros for Subjects, especially if it is compressed in the way mentioned in Part III. Making test macros, however, needs to be dropped in the way it is being used here, or at least used more sparingly. We could keep the setup macro because it sets up useful instance variables and makes reasonable guesses about the test suite’s environment. So, the macro idea should be kept back for special cases, the current implementation entirely dropped.
3. Nested, self-contained setup methods through sorcery method unbinding & stacking
I can’t help but be partial to the way I nested setup and teardown methods. It is my first actually useful innovation in Ruby—a trickery of method unbinding and stacking. Perhaps this bias makes me think it is still useful, but I honestly think it is a useful way to wrap Test::Unit. Let’s have a look at the way it is done in ActiveTest::Base:
class_inheritable_array :setup_methods
class_inheritable_array :teardown_methods
self.setup_methods = []
self.teardown_methods = []
# Execute all defined setup methods beyond Test::Unit::TestCase.
def setup_with_nesting
self.setup_methods.each { |method| method.bind(self).call }
end
alias_method :setup, :setup_with_nesting
# Execute all defined teardown methods beyond Test::Unit::TestCase.
def teardown_with_nesting
self.teardown_methods.each { |method| method.bind(self).call }
end
alias_method :teardown, :teardown_with_nesting
# Suck in every setup and teardown defined, unbind it, remove it and
# execute it on the child. From here on out, we nest setup/teardown.
def self.method_added(symbol)
unless self == ActiveTest::Base
case symbol.to_s
when 'setup'
self.setup_methods = [instance_method(:setup)]
remove_method(:setup)
when 'teardown'
self.teardown_methods = [instance_method(:teardown)]
remove_method(:teardown)
end
end
endThe first aspect which is useful is allowing setup and teardown nesting without affecting additions to Test::Unit made before or after loading ActiveTest. This is allowed by the code in method_added. Before ActiveTest is loaded, the methods and aliases already exist on Test::Unit; after ActiveTest is loaded, all setup or teardown methods are stacked in a class inherited array and subsequently undefined from the original class. When setup is finally called on a subclass of ActiveTest::Base, it is the ActiveTest::Base method called first. Here is an example stack:
class Test::Unit::TestCase
def setup; puts "a"; end;
end
class Test::Unit::TestCase
def setup_with_fixtures; puts "b"; end
alias_method_chain :setup, :fixtures
end
class ActiveTest::Sample < ActiveTest::Base
def setup; puts "c"; end
end
class SampleTest < ActiveTest::Sample
def setup; puts "d"; end
endUpon execution, it will output: d, c, b, then a. A caveat of this technique is that you must be absolutely certain that each unbound method setup_with_nesting executes must be rebound to the original class or one of its descendants. Because of the way class inherited attributes work, this rule is not violated: SampleTest can run the setup method from ActiveTest::Sample because it is a descendant, but other classes inheriting from ActiveTest::Sample will not have methods from SampleTest.
4. Specificity of breaking down things into Subjects
This is more of a design note than actual programming. Presently in Test::Unit, it is only possible to add to Test::Unit::TestCase, from which every test suite inherits. There is no way to say that, for example, assert_difference for an ActiveRecord test is slightly different to 'assert_difference for an ActionController test. The functionality may be the same (for example, refreshing the Record or refreshing the Controller request), but implemented differently for each. By creating subclasses of a test suite that provide only the methods needed by that kind of test, the developer can easily encapsulate test methods and not leak public methods across different kinds of tests. It’s just cleaner and will definitely form the backbone of test suites in the new ActiveTest framework.
Coming Up Next: Redesigning Weak Areas…

I do like that trick for stacking up setup/teardown. It’s one of the features of Perl 6’s object system that I’ve wished were available in Ruby on a few occasions now.