Apache POI 5.1.0出てた
の後続エントリ。
5.1.0でサイズは戻ったです。(少なくとも私が使う範囲は)
依存ツリーも4.1.2の頃とだいたい同じ見た目。 左が5.1.0で右が5.0.0(の前1/3くらい)です。これがまんまjarの数になる。
4.1.2 -> 5.1.0で見るとほとんど差はなくて、log4j2
にしたんだなーとか、commons-io
とかcommons-collections
使うようにしたんだなーとか。
そんなふわっとした見方ができたりします。
- Upgrade Batik dependency to 1.14
- Upgrade BouncyCastle dependency to 1.69 (including adding dependency on bcutil jar)
- Internal logging in POI now uses Apache Log4J 2
この辺か。
とりあえず4.1.2 -> 5.0.0 はちょっと抵抗感じたけど、5.1.0ならそこまででもない感覚。感覚があってるかはテストに任せる。
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" ); } }
@MethodSource
のvalue
要素は引数のファクトリメソッド名を指定しますが、テストメソッド名と同じであれば省略可能です。
ファクトリメソッドは引数無しのstatic
メソッドで、テストメソッドは引数ありとなるためシグネチャが同じにはなりません。
この例なら @MethodSource
じゃなく @ValueSource
が適切ですが、そこはお目こぼしください。
少し脱線になりますが、ファクトリメソッドの戻り型では Stream
の他に Collection
や配列などを使えますが、JUnitとしては「Stream
にできるもの」だったりします。(MethodSourceのJavadoc参照)
MethodSource
を使うと使われるMethodArgumentProviderでtoStreamしています。で、このtoStreamの中身はStreamだったらそのまま返すなので、「配列かList
かStream
かどれにすればいいんだろ」とか 無駄に 迷った時は Stream
にしておくのがいい感じです。迷わないなら何でもいいので、無理に Stream
を使う必要はありません。あと見ての通りStream
はclose
してくれないので、閉じなきゃいけない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'は定数および変数の宣言でのみ使用できます
なので以下のいずれかで対応していました。
@Nested
なクラスを@TestInstance(TestInstance.Lifecycle.PER_CLASS)
とした上で、ファクトリメソッドを非static
メソッドにする。- ファクトリメソッドを
static
で宣言できる場所に移動し、@MethodSource
で指定する。ここだと外に出してMyParameterizedTest#test
とFQNで指定する。
前者は検索すると目にしますが、テストインスタンスのライフサイクルはデフォルトの PER_METHOD
であってほしいので、やるにしても嫌な気分を背負いながらになります。後者はあまり見ないかな……ライフサイクルは変えなくていいんですが、せっかく省略できるファクトリメソッド名を指定しなきゃいけない上、FQNになるので冗長だし、外に出すとテストメソッドから離れてしまうしで、コーディング上でのやりたくない理由が多いんですよね。痛し痒しです。
どちらもユーザーガイドやJavadocをちゃんと読めば書いているんですが、全部読んでられないってのも実際のところかと思います。 あまり公式ドキュメントを読み慣れていない方はこういう機会に「このブログで書いてることって、どこにどういう風に書いているんだろう」と探してみると、他のことも「あの辺に書いてるんじゃないかな」とアタリがつけられるようになるかもしれません。ならないかもですが。
若干絡むか絡まないかの公式ドキュメントの読み方の話。
必要「だった」です
本題。
ここまで「必要 だった 」とか「コンパイルエラーに なりました。」とか過去形で書いてて、微妙な日本語になっています。
これは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はこの辺りもいい感じに警告してくれます。
警告が出ていることに慣れているような方もいるかと思いますが、IDEの警告0を常態化することを強くおすすめします。
公式ドキュメントを読めというけど
公式ドキュメントだからと言って、考えなしにそのコードを切り貼りしてはいけないんです。 っていう私の失敗談。露悪趣味はありません。
Javaの公式ドキュメントとしてJSLのAPI仕様(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がもっと目立つようにするんじゃないかな。ツリーを辿るっていうのと実行する操作は別の関心だと思うし。
教訓としては「公式ドキュメントかどうかに関わらず、コードをコピペで迂闊に試してはいけない」です。 ファイル削除だからまだいいけど、どっかにファイル送信するとかだったらえらいことです。流石にそんなコードはまともなドキュメントにはないだろうけど、どこぞのブログとかだったらあっても不思議じゃない。
当時のツイート
10年も前の話です。今でも思い出せる程度にはショッキングな出来事でした。Java7リリースは2011-07-28なので、リリース前ですね。 人間は失敗して成長するんですよ。失敗したからと言って成長するとは限らないけど。
どうにもならなかったから諦めた(諦められたという意味でどうにかはなってるのだけど)。
ついでに新しい話
そういえばJava 18でJavadocのコードスニペットが入ります。 JEP 413: Code Snippets in Java API Documentation です。
{@code}
とかは既にありますが、導入の動機となる現在の問題はJEPを参照ください。