I’ve been following the discussion on abstract method declaration with
some interest. It seems to me that the ideal implementation should not
only throw an exception on unimplemented methods, but also must pass
Matz’s example (implementation provided by base classes) and the
method_missing example.
With that in mind, I started to develope an implementation of
abstract_method in a test first way, where you write a test case and
then only implement enough code to make the test pass. Then you write
another test case and only add enough code to make that test pass.
Repeat as needed.
With that in mind, I came up with 5 test cases:
– BEGIN UNIT TESTS ------------------------------------------------
require ‘test/unit’
require ‘abstract_method’
class TestAbstractMethod < Test::Unit::TestCase
This is the basic use case for abstract methods where the abstract
method is declared in the parent class and redefined in the child
class.
def test_can_override_abstract_method
parent = Class.new {
abstract_method :foo
}
child = Class.new(parent) {
def foo
:foo_result
end
}
assert_equal :foo_result, child.new.foo
end
Here we make sure that not implementing the abstract method in the
child class will cause an exception when the method is invoked on
the child object. We don’t particularly care what exception is
thrown, but the exception message must mention the missing method
by name.
def test_unimplemented_abstract_method_throws_exception
parent = Class.new {
abstract_method :foo
}
child = Class.new(parent) {
}
begin
child.new.foo
fail “Oops”
rescue Exception => ex
assert_match /\bfoo\b/, ex.message
end
end
Now we make sure that our implementation passes Matz’s example
where an abstract method in a mixin is actually implemented in the
base class. We need to make sure that the mixin doesn’t hide the
implemented behavior.
def test_abstract_method_in_mixin_may_be_implemented_in_base_class
abstract_mixin = Module.new {
abstract_method :foo
}
parent = Class.new {
def foo
:foo_result
end
}
child = Class.new(parent) {
include abstract_mixin
}
assert_equal :foo_result, child.new.foo
end
This is a similar scenario to the previous test case where we make
sure the abstract declaration doesn’t interfer with implemented
behavior. This time the implemented behavior is provided by the
method missing technique.
def test_abstract_method_may_be_implemented_by_method_missing
parent = Class.new {
abstract_method :foo
}
child = Class.new(parent) {
def method_missing(sym, *args, &block)
:foo_result
end
}
assert_equal :foo_result, child.new.foo
end
Finally we want to ensure that +abstract_method+ can take multiple
method names, and that the method names may be either strings or
symbols.
def test_abstract_method_may_take_multiple_string_or_symbol_arguments
parent = Class.new {
abstract_method :foo, “bar”, “baz”
}
child = Class.new(parent) {
def foo
:foo_result
end
def bar
:bar_result
end
}
assert_equal :foo_result, child.new.foo
assert_equal :bar_result, child.new.bar
begin
child.new.baz
fail “Oops”
rescue Exception => ex
assert_match /\bbaz\b/, ex.message
end
end
end
– END UNIT TESTS -------------------------------------------------
And here is the implementation that came of that exercise:
– BEGIN ABSTRACT METHOD IMPLEMENTATION ---------------------------
class Module
def abstract_method(*method_names)
end
end
– END ABSTRACT METHOD IMPLEMENTATION -----------------------------
–
– Jim W.