clas3hiftの紹介
EasyMock( http://easymock.org/) やjMock( http://www.jmock.org/ ) がインスタンス渡しを行う場合にMockを作るのに対し、clas3hiftはクラス自体を差し替える作業をサポートする。
ただし現時点(2009/10)では差し替える事の出来るクラスに対して厳しい制限がある。
前提としてclas3hiftのjarにCLASSPATHを通しておく。JUnitのテストコードを例に挙げる。
まず junit.extensions.clas3hift.SandBox インターフェイスを実装したクラスを用意する。ここに実際のテストコードを記述する。実際にclas3hiftのソースツリーに含まれている test/SandBox1.java を示す。
package test; import static org.junit.Assert.*; import junit.extensions.clas3hift.SandBox; import java.util.Map; import test.stub.Foo0; public class SandBox1 implements SandBox { public void sandbox(Map<String, Object> result) { assertEquals("My name is Foo1", Foo0.getName()); } }
test.stub.Foo0 というのが差し替え対象のクラスであり、プロダクションコードで実際に使われるクラスになる。
他に test.stub.Foo1 というのを用意してあり、狙いとしてはこのSandBox1.sandbox()が実行される時には"Foo0"のアクセスが"Foo1"に裏側で切り替わるイメージになる。
上のassertに出てくる "My name is Foo1" というのがFoo1側の返す値で、Foo0の場合は "My name is Foo0" が返されるようになっている。
また、差し替えるテスト用のクラスは、差し替え元のクラスを継承している必要がある。
"Map<String, Object> result"というのは、実行時に後述のClas3hiftから渡されるHashMapで、sandbox中で直接assertするのではなく、sandboxの外でassertしたい場合に使う。具体的にはsandbox中で外部でassertしたい値をresultにセットしておけば、Clas3hift.result(String key) で取り出せるのでそれを使って外部でassertする。
このSandBox1はテストクラス側でインスタンス化し、clas3hiftの提供する特殊なClassLoaderから実行される。テストコード例を test/TestClas3hift.java から抜粋して示す。
package test; import org.junit.Test; import org.junit.Ignore; import static org.junit.Assert.*; import junit.extensions.clas3hift.SandBox; import junit.extensions.clas3hift.Clas3hift; import java.util.Map; import test.stub.Foo0; public class TestClas3hift { @Test public void Clas3hift_1() throws ClassNotFoundException { Clas3hift cs = new Clas3hift(); cs.register("test.stub.Foo0", "test.stub.Foo1"); SandBox1 sb = new SandBox1(); cs.play(sb); } // .... }
まず、clas3hiftのSandBox実行環境である Clas3hiftのインスタンスを生成する。
Clas3hift cs = new Clas3hift();
続いて、SandBox実行中に差し替えるクラスを、FQDNで指定する。元のコード中で使われるクラス名、テスト用に使いたいクラス名の順で指定知る。
cs.register("test.stub.Foo0", "test.stub.Foo1");
SandBoxインターフェイスを実装したクラスのインスタンスを生成する。
SandBox1 sb = new SandBox1();
Clas3hiftのplay(SandBox)メソッドにより、SandBoxが実行される。
cs.play(sb);
現時点(2009/10)では、SandBoxインターフェイスの実装方法には以下の制限がある。
コンストラクタを省略する事によるデフォルトコンストラクタか、引数無しのコンストラクタしか使えない。これはClas3hiftの内部実装の制限による。
インナークラス・ネストクラスにすると、Javaコンパイラにより外側のクラス・インスタンスへアクセスする為のメンバや、コンストラクタが自動的に追加される。
引数無しのコンストラクタしか受け付けられない為、よってインナークラス・ネストクラスとして定義する事は出来ない。
例:次のような書き方は出来ない。(本当はこれがしたかったんだけど・・・)
cs.play(new SandBox() { public void sandbox(Map result) { assertEquals("I am Foo1", Foo0.name); assertEquals("My name is Foo1", Foo0.getName()); });
現時点(2009/10)での内部実装を簡単に説明する。
Clas3hiftの独自クラスローダである junit.extensions.clas3hift.CL クラスの register() メソッドでクラスの差し替えが実装されている。
まず、差し替え元のクラスのバイトコードを取得し、クラス名をランダムで衝突しにくい名前にしてdefineClass()によりロードする。
test.stub.Foo0 → test.stub.Foo0$(衝突しにくいランダムコード)
次に差し替え先のクラスのバイトコードを取得する。そして以下の処理を行った後、defineClass()によりロードする。
(クラス名) test.stub.Foo1 → test.stub.Foo0 (親クラス名) test.stub.Foo0 → test.stub.Foo0$(衝突しにくいランダムコード)
これにより、独自クラスローダ内では "test.stub.Foo0"のstaticメソッドの実行が test.stub.Foo1 の該当staticメソッドに入れ替わる事になる。
もし将来的にSandBoxがインナークラス・ネストクラスで定義できるようになったとしても、そのインスタンスがテストメソッドから生成される以上、そのクラスローダもテスト実行環境のクラスローダになり、Clas3hift独自のクラスローダにはならない。
public void Clas3hift_1() throws ClassNotFoundException { System.out.println(this.getClass().getClassLoader()); // (1) SandBox1 sb = new SandBox1(); System.out.println(sb.getClass.getClassLoader()); // (2)
上のコードの (1), (2) は同じクラスローダになり、よって、そのままではクラスの差し替えは行えない。
ここでも独自クラスローダによるトリックが行われる。
独自クラスローダにより一旦SandBoxインスタンスのクラスがコピーされ、独自クラスローダにより再定義されたクラスでSandBoxインスタンスが再生成される。
最終的に実行されるのは、この独自クラスローダにより再定義されたSandBoxクラスであるため、これによりSandBox内のクラスの差し替えが行われる事になる。
cs.play(sb);
↓
public void play(SandBox sb) { SandBox sb_ = this._cl.surround(sb); sb_.sandbox(this._results); }
"this._cl" は junit.extensions.clas3hift.CL のインスタンスである。
public SandBox surround(SandBox sb) { // 元のSandBoxのClassオブジェクトを取得 Class srcClass = sb.getClass(); SandBox newSb = null; try { // この内部で、srcClassをコピーし、再定義(defineClass)している。 Class newClass = this.cloneClass(srcClass); /* if sandbox is defined as innerclass, its constructor * is private. so, powered by PA, call private constuctor. */ newSb = (SandBox)PA.instantiate(newClass, new Object[] {}); // これにより、クラスローダが独自CLのSandBoxインスタンスが作られる。 } catch (Throwable t) { t.printStackTrace(); } return newSb; }
最終的にこのnewSbのsandbox()メソッドの実行により、クラス差し替えが裏で行われる。
SandBox実装クラスの制限は、この新しいSandBoxクラスのインスタンス生成が現時点では、単純な引数無しでしか対応できていない事による。
PAライブラリを使っているのは、将来的にインナークラス・ネストクラス対応した場合に、protected/private/packageアクセスのコンストラクタでも対応可能にする為である・・・というのは建前で、本当は最初はstatic main内で実験していた為インナークラス・ネストクラスで実装していて、それで使っていた。JUnitのテストコードに載せた時に初めて、インナークラス・ネストクラスの特殊なコンストラクタに気づいた次第・・・。