DynaMo: Testing module to provide dynamic scope method overriding
Dynamic scope implementation for Method Overriding: override methods by specified context, only in current thread.
DON'T USE THIS GEM IN PRODUCTION CODE.
What's this?
To modify methods' behavior with its dynamic contexts, like this:
require 'dyna_mo'
module MyModule
class MyClass
attr_accessor :num
def initialize; @num = 0; end
def name; "name"; end
def sum(numbers); @num + numbers.reduce(:+); end
end
end
dynamo_define('MyModule::MyClass', :mytest_case_default) do
def_method(:initialize) do
@num = 1
end
def_method(:name) do
"dummyname"
end
def_instance_method(:name, :mytest_case1) do
"dummyname1"
end
def_method(:sum) do |numbers|
@num + numbers.reduce(:+) + 1
end
def_class_method(:create) do |init_num=0|
obj = self.new
obj.num = init_num
obj
end
end
class SynopsisTest < Test::Unit::TestCase
def test_synopsis
assert { MyModule::MyClass.new.name == "name" }
obj = MyModule::MyClass.new
assert { obj.num == 0 }
assert { obj.name == "name" }
dynamo_context(:mytest_case_default) do
assert { obj.num == 0 } # #initialize is not overridden
assert { obj.name == "dummyname" }
assert { MyModule::MyClass.new.name == "dummyname" }
assert { MyModule::MyClass.new.num == 1 }
assert { MyModule::MyClass.new.sum([1,2,3]) == (1+(1+2+3)+1) }
assert { MyModule::MyClass.create(100).num == 100 }
end
dynamo_context(:mytest_case1) do
assert { obj.name == "dummyname1" }
end
dynamo_define(MyModule::MyClass, :onetime_context) do
def_method(:name) do
"onetime"
end
end
dynamo_context(:onetime_context) do
assert { obj.name == "onetime" }
end
end
end
module MyModule; class MyClass2 < MyClass; end; end
class Synopsis2Test < Test::Unit::TestCase
def test_onece_more
dynamo_define(MyModule::MyClass, :onetime_context) do
def_method(:name) do
"onetime"
end
end
dynamo_context(:onetime_context) do
assert { MyModule::MyClass2.new.name == "onetime" }
end
end
end
Installation
Add this line to your application's Gemfile:
gem 'dyna_mo'
And then execute:
$ bundle
Or install it yourself as:
$ gem install dyna_mo
Usage
Require this module in helper.rb
or anywhere you want in test code.
require "dyna_mo"
Kernel.dynamo_define(name_or_module_instance, default_context_name, &block)
Create context to define instance methods and class methods of specified Module/Class.
-
name_or_module_instance
accepts both of String or Module/Class instance. But string specification must be name from top-level, likeNet::HTTP
(or::Net::HTTP
).
Given block is evaluated with a receiver instance of DynaMo::Contexts
.
dynamo_define(MyClass, :test_awesome_situation) do
# ...
end
DynaMo::Contexts#def_instance_method(method_name, context_name = default_context_name, &block)
Define instance method to override existing instance method, only in specified context. Instance variable reference like @data
is handled correctly for each objects.
super
cannot be used in this block. Use dynamo_super()
instead.
Given block is to be method body, and prepended on specified Module/Class, not rewrite method itself. So we can call original method definition by dynamo_super()
.
# in dynamo_define
dynamo_instance_method(:data, :my_test_context) do |num|
@data * num
end
obj.instance_eval{ @data = "abc" }
obj.data(3) #=> "abcabcabc"
The most recently called #def_instance_method
have highest priority. It's for ad-hoc definition in test code.
With def_instance_method
, given block can have arguments of arbitrary number, not same with original definition. But it brings very confusing behavior (especially for dynamo_super
), so it is not recommended for many cases.
This method also can add method which does NOT exists in original Module/Class definition.
DynaMo::Contexts#def_method(...)
Alias of def_instance_method
.
DynaMo::Contexts#def_class_method(method_name, context_name = default_context_name, &block)
Define class method. All other things are same with define_instance_method
.
Kernel.dynamo_context(context_name, &block)
Create dynamic scope in current thread for specified context name. Given block and lower stack calls run with overridden methods.
require "dyna_mo"
class A1; def self.label; "A"; end; end
class A2 < A1; def self.label; (super) * 2; end; end
dynamo_define(A1, :test) do
def_class_method(:label) do
"AB"
end
end
dynamo_context(:test) do
A2.label #=> "ABAB"
Thread.new { A2.label }.value #=> "AA"
end
We can apply 2 or more contexts at the same time. If these contexts have definition for same method, the recent defined one is called at first.
dynamo_define(A1, :test1) do
def_class_method(:label) do
"AB"
end
def_class_method(:label, :test2) do
"AC"
end
def_class_method(:label, :test3) do
"BC"
end
end
dynamo_context(:test2) do
dynamo_context(:test3) do
dynamo_context(:test1) do
A2.label #=> "BC"
end
end
end
Kernel.dynamo_super(*args)
Use to call original method definition in overriding method body (block). dyna_mo
method overriding is implemented with Module.prepend
, so dynamo_super()
calls original definition, not parent class's definition.
With dynamo_super
, all arguments must be specified explicitly.
dynamo_define(A1, :test_default) do
def_method(:name) do |prefix|
prefix + dynamo_super()
end
end
If you use this method with applying multi contexts, dynamo_super()
calls each overriding method bodies, like this:
dynamo_define(A1, :test1) do
def_class_method(:label) do
"1" + dynamo_super()
end
def_class_method(:label, :test2) do
"2" + dynamo_super()
end
def_class_method(:label, :test3) do
"3" + dynamo_super()
end
end
dynamo_context(:test1) do
dynamo_context(:test2) do
dynamo_context(:test3) do
# A1.label/test3 -> A1.label/test2 -> A1.label/test1 -> A1.label/(original)
A1.label #=> "321A"
end
end
end
Contributing
- Fork it ( https://github.com/[my-github-username]/dyna_mo/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Copyright
- License
- MIT
- Author
- @tagomoris