日々常々

ふつうのプログラマがあたりまえにしたいこと。

JUnitのNestedなMethodSourceの注意点

JUnit5での @MethodSource のおさらい

JUnit5にはパラメタライズドテスト用の @ParameterizedTest があり、様々な方法でパラメーターを与えられます。

その中でもパラメーターにある程度柔軟性が欲しい場合によく使うのが @MethodSource で、テストメソッドのパラメーターを生成できます。

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyParameterizedTest {

    @ParameterizedTest
    @MethodSource
    void test(String parameter) throws Exception {
        assertEquals(4, parameter.length());
    }

    static Stream<String> test() {
        return Stream.of(
                "hoge",
                "fuga",
                "piyo"
        );
    }
}

@MethodSourcevalue要素は引数のファクトリメソッド名を指定しますが、テストメソッド名と同じであれば省略可能です。 ファクトリメソッドは引数無しのstaticメソッドで、テストメソッドは引数ありとなるためシグネチャが同じにはなりません。

この例なら @MethodSource じゃなく @ValueSource が適切ですが、そこはお目こぼしください。

少し脱線になりますが、ファクトリメソッドの戻り型では Stream の他に Collection や配列などを使えますが、JUnitとしては「Streamにできるもの」だったりします。(MethodSourceのJavadoc参照) MethodSourceを使うと使われるMethodArgumentProviderでtoStreamしています。で、このtoStreamの中身はStreamだったらそのまま返すなので、「配列かListStreamかどれにすればいいんだろ」とか 無駄に 迷った時は Stream にしておくのがいい感じです。迷わないなら何でもいいので、無理に Stream を使う必要はありません。あと見ての通りStreamcloseしてくれないので、閉じなきゃいけないStreamは使っちゃだめです。

ここまではおさらい。

@MethodSource@Nested で注意が必要だったこと

JUnit5でネストしたテストを @Nestedで書きます。JUnit4のEnclosedではstaticなクラスでネストしましたが、JUnit5ではstatic無しです。 これにより外側のテストクラスのインスタンスフィールドが使用できるなど、色々便利になったのですが、@MethodSourceのファクトリメソッドはstaticになりますので、組み合わせると少々面倒なことになっていました。

class MyParameterizedTest {

    @Nested
    class MyNestedClass {

        @ParameterizedTest
        @MethodSource
        void test(String parameter) throws Exception {
            assertEquals(4, parameter.length());
        }

        static Stream<String> test() {
            return Stream.of(
                    "hoge",
                    "fuga",
                    "piyo"
            );
        }
    }
}

こう書きたいのですが、非staticなクラスでstaticなメソッドを持つことはできなかったので、コンパイルエラーになりました。エラーメッセージは以下。

.../MyParameterizedTest.java:36: エラー: 内部クラスMyParameterizedTest.MyNestedClassの静的宣言が不正です
        static Stream<String> test() {
                              ^
  修飾子'static'は定数および変数の宣言でのみ使用できます

なので以下のいずれかで対応していました。

  1. @Nested なクラスを @TestInstance(TestInstance.Lifecycle.PER_CLASS) とした上で、ファクトリメソッドを非staticメソッドにする。
  2. ファクトリメソッドをstaticで宣言できる場所に移動し、@MethodSourceで指定する。ここだと外に出して MyParameterizedTest#test とFQNで指定する。

前者は検索すると目にしますが、テストインスタンスのライフサイクルはデフォルトの PER_METHOD であってほしいので、やるにしても嫌な気分を背負いながらになります。後者はあまり見ないかな……ライフサイクルは変えなくていいんですが、せっかく省略できるファクトリメソッド名を指定しなきゃいけない上、FQNになるので冗長だし、外に出すとテストメソッドから離れてしまうしで、コーディング上でのやりたくない理由が多いんですよね。痛し痒しです。

どちらもユーザーガイドJavadocをちゃんと読めば書いているんですが、全部読んでられないってのも実際のところかと思います。 あまり公式ドキュメントを読み慣れていない方はこういう機会に「このブログで書いてることって、どこにどういう風に書いているんだろう」と探してみると、他のことも「あの辺に書いてるんじゃないかな」とアタリがつけられるようになるかもしれません。ならないかもですが。

若干絡むか絡まないかの公式ドキュメントの読み方の話。

irof.hateblo.jp

必要「だった」です

本題。

ここまで「必要 だった 」とか「コンパイルエラーに なりました。」とか過去形で書いてて、微妙な日本語になっています。

これはJava17(Java12-16は既にEOLなのでJava17を採用)でJEP 395: Recordsによりstaticメンバの定義が緩和されたためです。 興味のある方はJEP内の "Static members of inner classes" を参照ください。

これにより、上記のコードがコンパイルエラーにならなくなりました。 と言うことで、素直に動きます。「 @Nested@ParameterizedTest@MethodSource を使う時は @TestInstance(TestInstance.Lifecycle.PER_CLASS) にしたうえでファクトリメソッドを非staticにする」なんてことをやる必要はありません。このノウハウが必要なのはJava11を引き続き使用する場合のみです。Java17以降ではノイズになりますので、既存コードにある場合はサクッと消しておくようにしましょう。

いやぁ、ふつうに書けるっていいなぁ!と言うことでタイトルの「注意点」は「注意点がなくなった」です。

……Java17に上げられるならね。どうせユニットテストレベルだとJavaのバージョン違いでの差なんてほとんど出ないし、テストのコンパイルと実行だけ17にしていい……と思ったりしなくはない。テキストブロックもテストでは便利だし(特にJSON書くとき)。ビルドが複雑化するのを考えると微妙だけど。やっていいと思うけど、自分しかビルド環境メンテできない状況だと多分やらない。他の人がやろうとしたら応援する、くらいかなぁ。。。

書いた経緯

「なんか最近要らなくなったんだよなぁ」とふわっとした知識で話したんですが、JUnitのバージョンアップだと思ってて。でJUnitのリリースノートとか追ってみたんだけど見つからなくて。テストコード書いて見ようとしたらJava11だと書けなくて「あーそうだ、非staticには書けないよなー」と。でもJava17だと普通に書けたので、ああ緩和されたんだっけ、と。

Javaの仕様で必要だったフレームワークの使い方の迂回策が、Javaのバージョンアップでそもそも不要になっているわけです。 もちろん元のままでも動作しますが、この手のはしばらくすると理由が失われ「なんでこんなことをしているんだろう」となり、 理由がわからなくなると「消していいかどうかわからない」となると、おまじないとなって残ってしまいます。 過渡期にいる人は理由がわかるうちに対処する役割を担っているんじゃないかな、と思うわけです。

……とかそれっぽいこと書きましたが、単に「せっかく調べたから書いとこ」くらいのノリだったりします。

おまけ: IDEの警告

IntelliJ IDEAはこの辺りもいい感じに警告してくれます。

f:id:irof:20211118203830p:plain
Nestedがstaticになっちゃってるよー

f:id:irof:20211118203914p:plain
ファクトリメソッドがstaticじゃないよー

警告が出ていることに慣れているような方もいるかと思いますが、IDEの警告0を常態化することを強くおすすめします。

公式ドキュメントを読めというけど

公式ドキュメントだからと言って、考えなしにそのコードを切り貼りしてはいけないんです。 っていう私の失敗談。露悪趣味はありません。

Javaの公式ドキュメントとしてJSLAPI仕様(Javadocと呼ばれたりする)があります。 コード例が載ってると「挙動理解するためにとりあえず実行してみようか」とやったりします。

その対象になったのがこのコード。

https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/FileVisitor.html

Path start = ...
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
   @Override
   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
       throws IOException
   {
       Files.delete(file);
       return FileVisitResult.CONTINUE;
   }
   @Override
   public FileVisitResult postVisitDirectory(Path dir, IOException e)
       throws IOException
   {
       if (e == null) {
           Files.delete(dir);
           return FileVisitResult.CONTINUE;
       } else {
           // directory iteration failed
           throw e;
       }
   }
});

簡単に言えば rm -rf {指定したパス} です。なにそれこわい。

コードを読みさえすれば実行したら危ないのはわかるかなとは思うんですが、そういう危なさが判りづらいのがJavaの冗長と言われる所以だと思います。 仮にこれを今の自分が実装するなら、deleteがもっと目立つようにするんじゃないかな。ツリーを辿るっていうのと実行する操作は別の関心だと思うし。

教訓としては「公式ドキュメントかどうかに関わらず、コードをコピペで迂闊に試してはいけない」です。 ファイル削除だからまだいいけど、どっかにファイル送信するとかだったらえらいことです。流石にそんなコードはまともなドキュメントにはないだろうけど、どこぞのブログとかだったらあっても不思議じゃない。

当時のツイート

f:id:irof:20211108143118p:plain

10年も前の話です。今でも思い出せる程度にはショッキングな出来事でした。Java7リリースは2011-07-28なので、リリース前ですね。 人間は失敗して成長するんですよ。失敗したからと言って成長するとは限らないけど。

f:id:irof:20211108180430p:plain

どうにもならなかったから諦めた(諦められたという意味でどうにかはなってるのだけど)。

ついでに新しい話

そういえばJava 18でJavadocコードスニペットが入ります。 JEP 413: Code Snippets in Java API Documentation です。 {@code} とかは既にありますが、導入の動機となる現在の問題はJEPを参照ください。

技術から入ってもいいと言う話

システム開発の分野は技術の移り変わりが早く(これも他の分野と比べたことないので「早い気がする」と言うだけなのだけど)、なんらかの成功を収めた企業などの採用しているものがバズワードとなって一気に広まったりします。この時、その技術だけを追ってしまう現象がよく観測されます。だからバズワードになるんだけど。

批判的な意見

バズワードに飛びつくのは往々にしてアンチパターンです。例えば「モノリスからマイクロサービスへ」でも、明確な理由もなくマイクロサービスに飛びつくのは避けるよう書かれています。

書籍に限らず経験のある人は技術の螺旋を見通し、「この技術は結局のところ重要な課題を解決しない」と示唆する発言をしたりします。 強く辛辣な言葉もたまに見かけます。「そこに問題はないんだけどな」とか。

こう言う意見を無邪気に聞いてしまう人は、場合によっては「本質」と言う言葉に振り回されるようになります。 「本質はそこじゃないんだ」とか言うものの、見通せているかは危うかったりして。 いっちょ噛みで言う「本質」は「完全に理解した」と同義ですし、そんなすぐに本質を見抜けるのなんて卓越した技能を持った人だけです。居ないとは言わないけど、多分その一部ではないんじゃないかな。 そして結局何をすればいいのか途方に暮れているようなのも散見されます。

強そうな人の強い発言を目にしてしまうと、その技術を習得することに抵抗を覚えるでしょう。 これ自体は自然な気持ちだと思いますが、強い発言は広大な背景を持ってなされるものです。その背景は当人にとって当たり前のことなので、わざわざ語られたりもしません。 強い発言は往々にして正しく、説得力があります。経験から発せられる一本筋の通った発言に惹かれるのは当然ですが、その意見を取り入れられる土壌がない状態ではおそらく劇薬でしょう。

技術から入ってもいい

と言うことで、掲題の通り。技術から入ってもいいと思っています。

特定の技術は、特定の課題を解決するための模範解答の一つです。 短い記事やブログでなく、ある程度のボリュームの書籍や実用に耐えるツール、テンプレートなどまで昇華された技術には学ぶ価値があります。

対象の問題領域に対して知識が不足している状態で、問題だけを見てコアを掴めるのは一握りのモデリング能力を有した人だけでしょう。少なくとも私にはできない。 その問題を捉え、ある側面で切り取ったものが技術だと思います。 その特定の技術に精通すれば、解決しようとしている問題領域が、少なくともその技術の実装者が捉えたモデルが習得できます。 他の捉え方を身につけるのは、その視点を得てからでも遅くはありません。遅くはないはずです。早い遅いが何で決まるかは知らないけど。

とにかく その技術を最終到達点と認識しなければ大丈夫です。 逆に言えば、その技術があらゆる問題を解決する銀の弾丸(「銀の弾丸」は狼男にしか効かないので、万能ではないと思うんだけど、なんか万能の代名詞として使われますよね。なんでだろ。)やゴールデンハンマーのように捉えたとしたら大丈夫ではありません。先にあげた「批判的な意見」はこれを嗜めるものだと思っています。当人にはそんなつもりもなく、思ったこと言ってるだけだったりしますが。

繰り返します。知らないのならば、技術から入る選択肢を取っても構いません。 ただし入り口です。なので「技術 から 入ってもいい」です。 その技術を通して問題の輪郭を掴めたら、同じ問題に対した他の技術で照らし直してみたり、直接問題領域に飛び込んだりしてみることです。 次のステップがあることを意識するだけで、だいぶ違うと思います。

特定の技術に固執する癖があるならば、少なくともそこは改めることをお勧めします。 固執して突き進めるのは、技術ではなく問題を選べる場合です。卓越した技術があれば問題の方を選べます。問題を選べるようなら、技術への固執云々はたぶん気にしなくていいと思います。 私は問題にあった技術を選ぶのが好きなんで、その気持ちはわかりません。