きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Javaで最近使っているParameterizedTest【2021年版】

※ JUnit5のアップデートを知るための記事ではありません。私の知識アップデートをお伝えする記事になっています。


以前、ParameterizedTestに関する記事を書きました。その時は、テストコードの表現力を意識して、ArgumentsSourceを使ったほうが良いという結論を出していました。

しかし、もっとよりよい表現ができるテストが書けることを知ったので、それをアップデートした記事となります。

環境

  • Java
    • 15

ParameterizedTestのテスト表現方法を変更する

ParameterizedTestにはname変数があります。デフォルトでは、「[{index}]{argumentsWithNames}」となります。

f:id:nainaistar:20210514190004p:plain

name変数に値を設定することで、テストを表現できます。

@ParameterizedTest(name = "契約日が{0}, 解約日が{1}の時、{2}")

f:id:nainaistar:20210514190017p:plain

staticが必要なクラスでstaticを使わずに済むようにする

デフォルトではテストのインスタンスはメソッドごとに作成します。しかし、TestInstanceアノテーションを使うことで、クラスごとの生成に変更することもできます。メリットとしては、staticでなければ使えないBeforeAllAfterAll、ParameterizedTestで使用したいMethodSource等を使用できるようになります。

個人的にはNestedと組み合わせて使用でき、ネストしたクラス内に処理を閉じさせることができる点が好きです。

@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CanExpire {
  @MethodSource(value = "canExpire")
  @ParameterizedTest(name = "契約日が{0}, 解約日が{1}の時、{2}")
  void test_01(LocalDate start, LocalDate end, boolean result) {
    // 省略
  }

  private Stream<Arguments> canExpire() {
    return Stream.of();
  }
}

※ なお、Java17からは不要になったようです。

JUnitのNestedなMethodSourceの注意点 - 日々常々


具体的にTestInstanceを変更するメリットを知りたい人は、参考先のStackOverflowを読んでください。

ソースコード

テストコード: github.com

終わりに

ParameterizedTestname属性を追加するだけで、可読性が上がるので非常に便利です。以前のParameterizedTestも、値を一部だけ修正する方法が使えるので便利なんですがテスト結果の表示がちょっとだけイマイチでした。

ちゃんとテストを書きつつ、保守性が高いコードを書くためのテクニックを今後も磨いていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似

パラメータだけ変えて中身は同じテストをしたい(ParameterizedTest) nainaistar.hatenablog.com

参考

StackOverflow: What use is @TestInstance annotation in JUnit 5? stackoverflow.com

f:id:nainaistar:20210514190327p:plain

SpringのSpELでネストしたクラスの値をリフレクションして取得する

Java単体でもリフレクションで値を取得できます。しかし、ネストされた値を取得することは非常に面倒です。

Springではもっと簡単に値を取得することができます。それは、Spring式言語(Spring Expression Language、以後 SpELと表現します)を使用した方法です。

今回の記事では、SpELを活用してネストしたクラスの値をリフレクションして取得することを目指します。

環境

  • Java
    • 15
  • Spring Boot
    • 2.4.5
  • Lombok

ゴール

  • 別のクラスからリフレクションを使って値を取得する

前提

  • Getterがprivateではないこと

ユースケース

  • ネストするクラスから目的の変数を取得したい

例えば、「メールに必要な文言を巨大なネストするクラスから取得したい。しかも、簡単に書き換えられるようにマスタで管理したい。」という要件があったとします。

この要件を満たすために、マスタではchild.grandChild.taxという文言だけを管理しておいて、その値を元にリフレクションをする。

といったケースに使えると思います。

クラス構成

この記事ではユースケースを意識するために、親子関係をもつクラスを使用します。

- Parent
  - Child
    - GrandChild
@Value
@Builder
public class Parent {
  private Child child;
  private List<Child> children;
}
@Value
@Builder
public class Child {
  private GrandChild grandChild;
  private List<GrandChild> grandChildren;
}
@Value
@Builder
public class GrandChild {
  private String tax;
  private Map<String, Integer> animals = 
    Map.of("dog", 1, "cat", 3, "mouse", 10);  
}

ネストしないクラスをリフレクションで取得する

まずはもっとも簡単な方法です。GrandChildクラスのtax変数を取得します。

リフレクション対象のインスタンスをまずは生成します。

その後、StandardEvaluationContextのコンストラクタにインスタンスを渡して生成します。

最後にSpELを解釈するためのExpressionParserを生成し、リフレクションします。

@Test
void test_01_01() throws Exception {
  // リフレクション対象インスタンス生成
  var target = GrandChild.builder()
          .tax(123)
          .build();

  // StandardEvaluationContextを生成する
  StandardEvaluationContext context = new StandardEvaluationContext(target);
  // ExpressionParserを生成する
  ExpressionParser expressionParser = new SpelExpressionParser();
  // インスタンスの変数名「tax」を指定して取得する
  Object tax = expressionParser.parseExpression("tax").getValue(context);

  assertThat(tax).isEqualTo(123);
}

ネストするクラスをリフレクションで取得する

Parentクラスの変数のChildクラスの変数のGrandChildクラスのtax変数を取得します。SpELとしてはchild.grandchild.taxで表現します。

ネストしていない場合と処理はまったく変わりません。

@Test
void test_01() throws Exception {
  Parent target = Parent.builder()
      .child(Child.builder()
          .grandChild(GrandChild.builder()
              .tax(123)
              .build())
          .build())
      .build();

  StandardEvaluationContext context = new StandardEvaluationContext(target);
  ExpressionParser expressionParser = new SpelExpressionParser();
  Object tax = expressionParser.parseExpression("child.grandChild.tax").getValue(context);

  assertThat(tax).isEqualTo(123);
}

リストやマップの項目をリフレクションで取得する

処理は同じですので、SpELのみ記載します。Java単体だとコレクション処理が面倒なので、非常に嬉しいですね。

リストの項目を取得する

配列の順番を指定して取得できます。

  • children[0].grandChildren[0].tax

マップの項目を取得する

マップのKeyを指定することで、対応するValueを取得することができます。

  • children[0].grandChildren[0].animals['dog']

SpEL式をもっと使いこなす

単純なリフレクションだけでなく、SpELを使用することができます。無理に使うケースは無いと考えていますが、一応覚えておきましょう。

次の例は、マップのサイズが1より大きい場合はtrueを返却するSpELです。

  • children[0].grandChildren[0].animals.size() > 1

SpELをもっと知りたい場合は、こちらのページが参考になりました。

www.baeldung.com

ソースコード

実装コード https://github.com/hirotoKirimaru/cucumber-sample/blob/63230957325a487f23c6800457f5565506e9c551/src/test/java/kirimaru/biz/domain/ReflectionTests.java#L26

終わりに

Springの機能を使用すると簡単にリフレクションをすることができます。リフレクション = SpELではないので、なかなか検索されないワードだと思いますが、便利ですのでぜひ使ってみてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

baeldung: Spring Expression Language Guide www.baeldung.com

f:id:nainaistar:20210507182249p:plain

Javaでリフレクションを素振りする

基本的にはIDEのサポートが得られなかったり、静的解析してくれないので、リフレクションは使ってはいけません。しかし、非推奨であることと、使えないことは違います。

当記事では、Javaでリフレクションを使ってprivateメソッドとフィールドを呼び出せることを目標とします。

環境

  • Java
    • 15
  • Lombok

ゴール

  • 別のクラスからリフレクションを使ってprivateメソッドを呼び出す
  • 別のクラスからリフレクションを使ってprivateフィールドを取得する
  • 別のクラスからリフレクションを使ってprivateフィールドを設定する

ユースケース

  • テストクラスでprivateメソッドを実行したい
    • そもそも、テストしたいメソッドはパッケージプライベートか、protectedにしましょう。
  • 巨大なネストするクラスから目的の変数を取得したい

クラス構成

この記事ではユースケースを意識するために、親子関係をもつクラスを使用します。

- Parent
  - Child
    - GrandChild
@Value
@Builder
public class Parent {
  private Child child;
}
@Value
@Builder
public class Child {
  private GrandChild grandChild;
  private List<GrandChild> grandChildren;
}
@Value
@Builder
public class GrandChild {
  private String description;
  
  // 引数無し
  private String getDescription() {
    return description;
  }

  // 引数あり
  private int computeMultiple(int num1, int num2) {
    return num1 * num2;
  }

  // 可変長引数の引数あり
  private int computeMultipleArray(int... nums) {
    int tmp = 1;
    for (int num : nums) {
      tmp *= num;
    }
    return tmp;
  }

引数無しのprivateメソッドを実行する

まずはリフレクション対象のインスタンスを生成します。

次にインスタンスから、リフレクションしたいprivateメソッドを取得します。一度getClassでクラスを取得してから、getDeclaredMethodでメソッドを取得できます。

次に、setAccessibleで可視性を変更します。実行しない場合は、java.lang.IllegalAccessExceptionが発生し、可視性を変更する必要があるとメッセージが出力されます。

最後に、取得したprivateメソッドのinvokeにインスタンスをパラメータ渡すと実行することができます。戻り値がある場合は、Object型で返却されるので必要に応じてキャストしてください。

@Test
void test_01() throws Exception {
  // リフレクション対象のインスタンス生成
  GrandChild target = GrandChild.builder()
      .description("説明")
      .build();

  // メソッドを取得する
  Method method = target.getClass().getDeclaredMethod("getDescription");
  // privateメソッドにアクセスできるようにする
  method.setAccessible(true);
  // インスタンスからメソッドを実行して値を取得する
  String result = (String) method.invoke(target);
  assertThat(result).isEqualTo("説明");
}

引数ありのprivateメソッドを実行する

引数無しのprivateメソッドの実行との比較差分は2つです。

  • メソッドを指定するときにパラメータの数と型を指定すること
  • invokeメソッドに実際のパラメータを渡すこと

Javaには同名クラスでパラメータが異なるオーバーロードがありますので、面倒くさがらずに設定する必要があります。

@Test
void test_03() throws Exception {
  // リフレクション対象のインスタンス生成
  GrandChild target = GrandChild.builder()
      .build();

  // メソッドを取得する
  Method method = target.getClass()
    .getDeclaredMethod("computeMultiple", int.class, int.class);
  // privateメソッドにアクセスできるようにする
  method.setAccessible(true);
  // インスタンスからメソッドを実行して値を取得する
  int result = (int) method.invoke(target, 100, 20);
  assertThat(result).isEqualTo(2000);
}

なお、可変長引数の場合は、内部的には配列になっていますので、配列を渡しましょう。リフレクションを使わない普通のメソッドであれば可変長でパラメータを渡せますが、リフレクション時には明示的に配列にしなければいけない点がポイントです。

@Test
void test_04() throws Exception {
  GrandChild target = GrandChild.builder()
      .build();

  Method method = target.getClass()
    .getDeclaredMethod("computeMultipleArray", int[].class);
  method.setAccessible(true);
  int result = (int) method.invoke(target, new int[]{100, 20, 3, 4});
  // この渡し方はできない
// int result = (int) method.invoke(target, 100, 20, 3, 4);
  assertThat(result).isEqualTo(24000);
}

privateなフィールドを取得したい

基本的にはメソッドと同じような処理です。getDeclaredMethodではなく、getDeclaredFieldを使用します。getDeclaredFieldを元に```get````すると値を取得できます。

    @Test
    void test_02_02() throws Exception {
      GrandChild target = GrandChild.builder()
          .tax(123)
          .build();

      Field fieldTax = target.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(target);

      assertThat(tax).isEqualTo(123);
    }

配列項目を参照する場合は、取得した値を元にgetする必要がありそうです。

    @Test
    void test_02_03() throws Exception {
      Child target = Child.builder()
          .grandChildren(List.of(
              GrandChild.builder()
                  .tax(123)
                  .build()
          )).build();

      Field fieldGrandChildren = target.getClass().getDeclaredField("grandChildren");
      fieldGrandChildren.setAccessible(true);
     // 一旦Listにキャスト
      List grandChildren = (List)fieldGrandChildren.get(target);
     // 1個目の配列を取得する
      Object grandChild = grandChildren.get(0);
      Field fieldTax = grandChild.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(grandChild);

      assertThat(tax).isEqualTo(123);
    }

privateなフィールドを設定したい

似たような流れなので、特に解説はしません。

    @Test
    void test_02_04() throws Exception {
      GrandChild target = GrandChild.builder()
          .build();

      Field fieldTax = target.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      fieldTax.set(target, 123);

      assertThat(target.getTax()).isEqualTo(123);
    }

巨大なネストするクラスから目的の変数を取得したい

この項目の新規性はありません。ユースケースを意識したものとなります。

例えば、「メールに必要な文言を巨大なネストするクラスから取得したい。しかも、簡単に書き換えられるようにマスタで管理したい。」という要件があったとします。

この要件を満たすために、マスタではchild.grandChild.taxという文言だけを管理しておいて、その値を元にリフレクションをするようにします。

今回の例では、ParentクラスからChildクラスを取得し、ChildクラスからGrandChildクラスを取得し、GrandChildクラスのtax変数を取得します。

@Test
void test_02() throws Exception {
  Parent target = Parent.builder()
      .child(Child.builder()
          .grandChild(GrandChild.builder()
              .tax(123)
              .build())
          .build())
      .build();

      Field childFiled = target.getClass().getDeclaredField("child");
      childFiled.setAccessible(true);
      Object child = childFiled.get(target);
      Field childField = child.getClass().getDeclaredField("grandChild");
      childField.setAccessible(true);
      Object grandChild = childField.get(child);
      Field fieldTax = grandChild.getClass().getDeclaredField("tax");
      fieldTax.setAccessible(true);
      Object tax = fieldTax.get(grandChild);

  assertThat(tax).isEqualTo(123);
}

ソースコード

実装コード https://github.com/hirotoKirimaru/cucumber-sample/blob/fba4d7c4c0b8140982c7032e15cdba328613c3ed/src/test/java/kirimaru/biz/domain/ReflectionTests.java#L81a

終わりに

リフレクションは基本使いたくありませんが、たまに使いたくなる時があります。

実際は、Springの機能でもうちょっと簡単にリフレクションで値を取得できたりするのですが、それはまた別の記事にてご紹介させてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

何かの時にスッと使える力技 - Reflection 編 qiita.com

リフレクション www.ne.jp

f:id:nainaistar:20210505112601p:plain