ちょっとしたトラブルシュート、デバッグに使っているものに、Bytemanというツールがあります。
Byteman
http://byteman.jboss.org/
Bytemanは、Javaのバイトコードを書き換えてコードを注入したりすることができます。
個人的にも、エントリを書いたことが。
バイトコード操作ツール、Bytemanを試す
http://d.hatena.ne.jp/Kazuhira/20131022/1382455739
で、このBytemanですが、普段自分が使う時には、スクリプトを用意して実行中のJavaプロセスにアタッチ、もしくはJava起動時にエージェントとしてあらかじめ適用する形で使用することが多いのですが、BMUnitというものを使用するとこのBytemanとテスティングフレームワークを統合できるようです。
BMUnit : Using Byteman with JUnit or TestNG from maven and ant
https://developer.jboss.org/wiki/BMUnitUsingBytemanWithJUnitOrTestNGFromMavenAndAnt#top
JUnit、TestNGと一緒に使えるのだとか。こちらは存在は知っていたのですが、試したことがなかったのでこの機会にと。
それでは、使ってみましょう。基本的には、上記のチュートリアルを見て進めればOKです。
また、テストコードもサンプルとしては参考になります。JUnitと合わせた例はこちら。
https://github.com/bytemanproject/byteman/blob/master/contrib/bmunit/test/src/test/UnitTest.java
Maven依存関係
依存関係としては、少なくとも以下があればよいみたいです。
<dependency> <groupId>org.jboss.byteman</groupId> <artifactId>byteman-bmunit</artifactId> <version>2.2.1</version> <scope>test</scope> </dependency>
チュートリアルに載っているpomでは他にもいろいろ指定していますが、これだけを書いていても依存関係としてそれらを引っ張ってきてくれます。
テスティングフレームワークは、個人の好みでJUnitとAssertJ。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency>
テスト対象コード
それでは、テスト対象として簡単なクラスを用意します。今回作ったのは、こちら。
////////////////////////////////////////////////// // src/main/java/example/CalcService.java package example; public class CalcService { public int add(int a, int b) { return a + b; } public int multiply(int a, int b) { return a * b; } } ////////////////////////////////////////////////// // src/main/java/example/StringJoinService.java package example; public class StringJoinService { public String join(String separator, String... tokens) { return String.join(separator, tokens); } }
これらに対して、テストコードを書きます。
ルールスクリプトを用意
が、テストコードの前に、Bytemanのルールスクリプトを用意しましょう。
今回は、このようなものを用意しました。スクリプトの配置ディレクトリは、「bm-scripts」としています。
bm-scripts/calc-service.btm
RULE class level trace rule@CalcService#add CLASS example.CalcService METHOD add AT EXIT IF TRUE DO traceln("CalcService#addにテストクラスレベルでスクリプト適用 a = " + $1 + ", b = " + $2 + ", return = " + $!); ENDRULE RULE class level trace rule@CalcService#multiply CLASS example.CalcService METHOD multiply AT EXIT IF TRUE DO traceln("CalcService#multiplyにテストクラスレベルでスクリプト適用 a = " + $1 + ", b = " + $2 + ", return = " + $!); ENDRULE
こちらは、CalcServiceというクラスのメソッドに対して適用するスクリプト。対象のメソッドを抜ける時に、ログ出力します。
テストクラス上では、クラス全体に適用します。
bm-scripts/string-join-service.btm
RULE method level trace rule@StringJoinService#join CLASS example.StringJoinService METHOD join AT EXIT IF TRUE DO traceln("StringJoinService#joinにテストメソッドレベルでスクリプト適用 separator = [" + $1 + "], tokens = " + java.util.Arrays.toString($2) + ", return = " + $!); ENDRULE
こちらは、StringJoinServiceというクラスのメソッドに対して適用するスクリプト。やっぱり、対象のメソッドを抜ける時に、ログ出力します。
テストクラス上では、テストメソッドに適用します。
それぞれ、適用タイミングは、メソッドから抜ける時。スクリプトの記述方法については、前述のエントリやオフィシャルサイトをご覧ください。
テストクラス、テストメソッドに対して適用する、と書きましたが、それは便宜的な話で実際にはアノテーションで適用箇所を指定することになります。
テストコード
それでは、テストコードを書きます。
まずは、CalcServiceというクラスに対するテストコード。
src/test/java/example/CalcServiceTest.java
package example; import static org.assertj.core.api.Assertions.*; import org.jboss.byteman.contrib.bmunit.*; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(BMUnitRunner.class) // 以下の3つの書き方、どれでもOK @BMScript(dir = "bm-scripts", value = "calc-service.btm") //@BMScript(dir = "bm-scripts", value = "calc-service") //@BMScript("bm-scripts/calc-service") public class CalcServiceTest { @Test @BMRule(name = "method level trace rule@CalcService#add", targetClass = "example.CalcService", targetMethod = "add", targetLocation = "EXIT", action = "traceln(\"CalcService#addにテストメソッドレベルでスクリプト適用 a = \" + $1 + \", b = \" + $2 + \", return = \" + $!);") public void testCalcService1() { CalcService sut = new CalcService(); assertThat(sut.add(1, 2)) .isEqualTo(3); assertThat(sut.multiply(3, 4)) .isEqualTo(12); } @Test @BMRule(name = "method level trace rule@CalcService#multipl", targetClass = "example.CalcService", targetMethod = "add", targetLocation = "EXIT", action = "traceln(\"CalcService#multiplyにテストメソッドレベルでスクリプト適用 a = \" + $1 + \", b = \" + $2 + \", return = \" + $!);") public void testCalcService2() { CalcService sut = new CalcService(); assertThat(sut.add(5, 6)) .isEqualTo(11); assertThat(sut.multiply(7, 8)) .isEqualTo(56); } }
テストメソッドの中身は大したことありませんが、JUnit以外に付いているアノテーションなどについて少し説明。
最初に、BMUnitを使うためのimport文はこちらになります。
import org.jboss.byteman.contrib.bmunit.*;
端折って「*」にしていますが、このパッケージで完結するでしょう。
クラスには@RunWithアノテーションを指定し、BMUnitRunnerクラスを指定します。
@RunWith(BMUnitRunner.class)
続いて、クラスの宣言に@BMScriptアノテーションを付与しています。
// 以下の3つの書き方、どれでもOK @BMScript(dir = "bm-scripts", value = "calc-service.btm") //@BMScript(dir = "bm-scripts", value = "calc-service") //@BMScript("bm-scripts/calc-service")
これで、このクラス全体に対して「bm-scripts/calc-service.btm」スクリプトの内容が適用されます。3種類の書き方をしていますが、どれでも動きます。
なお、@BMScriptアノテーション自体はメソッドにも付与可能みたいです。
メソッドレベルでは、@BMRuleアノテーションを付与してルールスクリプトをJavaコードに直接定義しています。
@Test @BMRule(name = "method level trace rule@CalcService#add", targetClass = "example.CalcService", targetMethod = "add", targetLocation = "EXIT", action = "traceln(\"CalcService#addにテストメソッドレベルでスクリプト適用 a = \" + $1 + \", b = \" + $2 + \", return = \" + $!);") public void testCalcService1() {
こちらも、クラスの宣言に付与することも可能です。ちなみに、targetLocationを指定しないとうまく認識されませんでした…。サンプルにはほとんど書いていないのですが??
もうひとつテストメソッドにルールを定義していますが、こちらは割愛。
あと、これらを取りまとめる@BMScripts、@BMRulesなどもあります。
では、テストを動かしてみましょう。
$ mvn test
すると、こんな内容が出力されます。
Running example.CalcServiceTest Setting org.jboss.byteman.allow.config.update=true CalcService#addにテストクラスレベルでスクリプト適用 a = 1, b = 2, return = 3 CalcService#addにテストメソッドレベルでスクリプト適用 a = 1, b = 2, return = 3 CalcService#multiplyにテストクラスレベルでスクリプト適用 a = 3, b = 4, return = 12 CalcService#addにテストクラスレベルでスクリプト適用 a = 5, b = 6, return = 11 CalcService#multiplyにテストメソッドレベルでスクリプト適用 a = 5, b = 6, return = 11 CalcService#multiplyにテストクラスレベルでスクリプト適用 a = 7, b = 8, return = 56
ちょっとコメントを入れると
### testCalcService1で出力した内容 CalcService#addにテストクラスレベルでスクリプト適用 a = 1, b = 2, return = 3 CalcService#addにテストメソッドレベルでスクリプト適用 a = 1, b = 2, return = 3 CalcService#multiplyにテストクラスレベルでスクリプト適用 a = 3, b = 4, return = 12 ### testCalcService2で出力した内容 CalcService#addにテストクラスレベルでスクリプト適用 a = 5, b = 6, return = 11 CalcService#multiplyにテストメソッドレベルでスクリプト適用 a = 5, b = 6, return = 11 CalcService#multiplyにテストクラスレベルでスクリプト適用 a = 7, b = 8, return = 56
となります。それぞれ、CalcServiceの各メソッドにAOP的な感じで処理が練り込まれています。
また、クラス全体で適用されているルールと、メソッド個別に適用されているルールがあることがわかります。
もっとサンプル
今度は、テストメソッドに作成済みのルールスクリプトを適用しているコード。
src/test/java/example/StringJoinServiceTest.java
package example; import static org.assertj.core.api.Assertions.*; import org.jboss.byteman.contrib.bmunit.*; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(BMUnitRunner.class) public class StringJoinServiceTest { @Test @BMScript("bm-scripts/string-join-service") public void testStringJoinService() { StringJoinService sut = new StringJoinService(); assertThat(sut.join(", ", "Hello", "World")) .isEqualTo("Hello, World"); } }
テスト実行時に出力される結果は、このように。
Running example.StringJoinServiceTest StringJoinService#joinにテストメソッドレベルでスクリプト適用 separator = [, ], tokens = [Hello, World], return = Hello, World
Javaのクラスに対して適用する
このように便利なBytemanですが、java.langパッケージ階層のクラスに対してルールを適応する場合は、ちょっと細工が必要です。
ルールファイルを用意します。
bm-scripts/jdk-class.btm
RULE JDK class trace rule CLASS java.lang.Integer METHOD decode AT ENTRY IF TRUE DO traceln("Integer#decode, param = " + $1); ENDRULE
ここでは、Integer#decodeを対象とします。
テストコードを作成。
src/test/java/example/JdkClassTest.java
package example; import static org.assertj.core.api.Assertions.*; import java.util.*; import org.jboss.byteman.contrib.bmunit.*; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(BMUnitRunner.class) public class JdkClassTest { @Test @BMScript("bm-scripts/jdk-class") public void testJdkClass() { assertThat(Integer.decode("100")) .isEqualTo(100); } }
テストを実行します。
$ mvn test
が、何も出力されません。
Running example.JdkClassTest
これらのクラスに適用するためには、「-Dorg.jboss.byteman.transform.all」システムプロパティを指定する必要があります。
では、気を取り直してもう1度。
$ mvn test -Dorg.jboss.byteman.transform.all
今度は、うまくルールが適用されます。
Running example.JdkClassTest Integer#decode, param = 100
アノテーションでルールをJavaコードに埋め込むところは若干ハマりましたが、使い方がわかってしまえば大丈夫だと思います。
Javaの起動引数に指定する方法と合わせて、活用していきたいと思います。