CLOVER🍀

That was when it all began.

BMUnitを使って、BytemanとJUnitを合わせて使う

ちょっとしたトラブルシュート、デバッグに使っているものに、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の起動引数に指定する方法と合わせて、活用していきたいと思います。