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 -----------------------------