新世代のビルドツール Bazel で Java プロジェクトのビルドからユニットテストまで
この記事はアプレッソ Advent Calendar 2016 19日目のエントリーです。
新世代のビルドツール Bazel を使って、Java プロジェクトをビルドしてみたいと思います。
Bazel とは?
Bazel とは、Google 社内で使われているビルドツール Blaze をオープンソース化したものです。
2016.12 現在 Beta 版として公開されており、2017.4Q 公開予定の Stable 版へ向けて着々と開発が進んでおります。
Bazel の特徴
公式ページ によると、Speed、Scalability、Flexibility、Correctness、Reliability、Repeatability という6つの軸で語られています。
- 最適化された依存性分析、高度なキャッシング、ビルドアクションの並列化による Speed
- ヤン○ーディー○ルばりに「小さなものから大きなものまで」カバーできる Scalability
- さまざまな言語やプラットフォームをカバーし、拡張可能なルールフレームワークから来る Flexibility
- タイムスタンプだけでなく依存関係グラフを調べてビルドタイミングを決定する Correctness
- Google のエンジニアリング環境で長年に渡って洗練されてきた Reliability
- 明示的に宣言されている入力ファイルのみを使用し、必要最小限のファイルのみを含むサンドボックス環境で実行されることによる Repeatability
Java で作られてはいますが、ビルド対象の言語は Java に限らず、C/C++、Objective-C、Python、C#、Go、Scala など様々な言語を組み込みでサポートしており、Android や iOS を含むクライアントサイドから AppEngine などのサーバサイドまで広範囲なプラットフォームに対応する方針が取られています。
また、独自のビルドルールを開発可能なフレームワークも用意されているようで、拡張性も問題なさそうです。
ビルド環境の構築
能書きはこのくらいにして、さっそく試してみましょう。まずはビルド環境を構築してみます。
Beta 版時点で Bazel がサポートしているビルドマシン環境は以下になります。
- Ubuntu Linux
- Mac OS X
- Windows(experimental)
ソースからコンパイルすることで上記以外の環境でも使えるっぽいです。
今回は AWS 上に構築した Ubuntu で環境を作ってみます。Ubuntu 自体の構築はマネジメントコンソールからポチポチやるだけなので省略します。
JDK 8 のインストール
Bazel を使用するためには、JDK 8 のインストールが必要になります。JDK 7 でもイチオー使えるらしいですが、非推奨になっています。
Ubuntu 上で以下のコマンドを実行します。
$ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update $ sudo apt-get install oracle-java8-installer $ java -version java version "1.8.0_111" Java(TM) SE Runtime Environment (build 1.8.0_111-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)
これで /usr/bin/java が作られるので、環境変数の追加は不要です。
ってゆーか apt-get install で入れると /etc/profile.d/jdk.sh まで作ってくれるんですね。rpm もこれやってくれませんかね。。。
Bazel のインストール
続いて Bazel 自体のインストールです。次のコマンドを実行します。
$ echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list $ curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - $ sudo apt-get update && sudo apt-get install bazel $ sudo apt-get upgrade bazel $ bazel version Build label: 0.4.2 Build target: bazel-out/local-fastbuild/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar Build time: Wed Dec 7 18:47:11 2016 (1481136431) Build timestamp: 1481136431 Build timestamp as int: 1481136431
こちらも /usr/bin/bazel まで作ってくれるので環境変数の追加は不要です。
これで Bazel を試してみる環境が出来上がりました。簡単ですね!
Java プロジェクトのビルド ~基本編~
それでははりきって、Java プロジェクトをビルドしてみましょう。
構成はこんな感じです。
Java ソースコード
HelloBazel.java
package willard379.bazel.sample; public class HelloBazel { public static void main(String[] args) { if (args.length > 0) { System.out.println(String.format("Hello, %s!", args[0])); } } }
なんの変哲もない、はろ~わ~るどです。
WORKSPACE ファイル
ここからが Bazel 固有の設定になります。
Bazel でビルドをするためには、ビルド対象の資材が含まれるトップディレクトリに WORKSPACE という名前のファイルを作る必要があります。 Bazel に対して「ここがルートディレクトリやで!」と教えてあげるわけですね。
後で出てくる依存性の追加などはこのファイルに設定することになります。現時点では特に設定するものがないので、空ファイルを置いておきます。たとえ設定するものがなくてもファイル自体は存在している必要があるのでご注意を。
WORKSPACE
(空ファイル)
BUILD ファイル
ルートディレクトリにもうひとつ、BUILD という名前のファイルを作成します。これは Bazel に行わせるビルドの命令をを記述するファイルで、Python に似た DSL でルールと呼ばれるビルドシーケンスを記述します。詳しくはこちら を参照。
BUILD
java_binary( name = "HelloBazel", srcs = glob(["src/main/java/**/*.java",]), main_class = "willard379.bazel.sample.HelloBazel", )
main()
メソッドを持つ Java アプリケーションをビルドするためのルールは、 java_binary()
になります。
name
属性で任意の名前を定義します。後々ここで定義した名前を使って Bazel のビルドコマンドを叩いたりするので、アプリケーションの一意な識別子という認識でよいでしょう。
srcs
属性では、ビルド対象のソースファイルを指定します。glob()
関数を使うことで、パターンにマッチするファイルを一括で指定することができます。
main_class
属性では、 main()
メソッドを持つ Java クラスを FQCN で指定します。これが後述の bazel run
コマンドから呼び出されるエントリポイントになります。
ビルドの実行
これで最低限ビルドが実行できる準備が整いましたので、ビルドを実行してみましょう。 Ubuntu で WORKSPACE ファイルが置かれているルートディレクトリに移動し、以下のコマンドを実行します。
$ bazel build //:HelloBazel INFO: Found 1 target... Target //:HelloBazel up-to-date: bazel-bin/HelloBazel.jar bazel-bin/HelloBazel INFO: Elapsed time: 9.064s, Critical Path: 1.05s
ビルドの指示は bazel build
コマンドで行います。"//:HelloBazel" の部分は、「"//" + BUILD ファイルが配置されたディレクトリへのパス + ":" + java_binary()
の name
で指定した識別子」になります。
今回、BUILD ファイルは WORKSPACE ファイルと同じ階層に置かれているので、"//" の後にすぐ ":" がきますが、マルチプロジェクト構成の場合など BUILD ファイルを複数作る場合には、ルートディレクトリから BUILD ファイルが配置されているディレクトリへの相対パスを指定します。
ちなみに今回のように WORKSPACE ファイルと BUILD ファイルが同じ階層に置かれている場合には "//:" を省略することもできます。
BUILD SUCCESSFUL 的なメッセージがほしいところですが、エラーが出ていないのでビルドが成功したようです。 ビルドの成果物を見てみましょう。
$ ll total 52 drwxrwxr-x 4 ubuntu ubuntu 4096 Dec 18 12:09 ./ drwxrwxr-x 5 ubuntu ubuntu 4096 Dec 18 04:15 ../ lrwxrwxrwx 1 ubuntu ubuntu 122 Dec 18 12:09 bazel-bin -> /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/HelloBazel/bazel-out/local-fastbuild/bin/ lrwxrwxrwx 1 ubuntu ubuntu 127 Dec 18 12:09 bazel-genfiles -> /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/HelloBazel/bazel-out/local-fastbuild/genfiles/ lrwxrwxrwx 1 ubuntu ubuntu 90 Dec 18 12:09 bazel-HelloBazel -> /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/__main__/ lrwxrwxrwx 1 ubuntu ubuntu 100 Dec 18 12:09 bazel-out -> /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/__main__/bazel-out/ lrwxrwxrwx 1 ubuntu ubuntu 127 Dec 18 12:09 bazel-testlogs -> /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/HelloBazel/bazel-out/local-fastbuild/testlogs/ -rw-rw-r-- 1 ubuntu ubuntu 132 Dec 18 12:06 BUILD -rw-rw-r-- 1 ubuntu ubuntu 236 Dec 18 12:06 .classpath drwxrwxr-x 8 ubuntu ubuntu 4096 Dec 18 12:07 .git/ -rw-rw-r-- 1 ubuntu ubuntu 195 Dec 18 12:06 .gitignore -rw-rw-r-- 1 ubuntu ubuntu 369 Dec 18 12:06 .project drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 18 12:06 src/ -rw-rw-r-- 1 ubuntu ubuntu 0 Dec 18 12:06 WORKSPACE
シンボリックリンクがいくつかできています。Bazel では、ビルドの成果物はワークスペースの下ではなく、~/.cache/bazel の下に出力されます。jar ファイルは bazel-bin シンボリックリンクの先にあります。
$ ll bazel-bin/ total 160 drwxrwxr-x 4 ubuntu ubuntu 4096 Dec 18 12:22 ./ drwxrwxr-x 5 ubuntu ubuntu 4096 Dec 18 12:22 ../ -r-xr-xr-x 1 ubuntu ubuntu 8175 Dec 18 12:22 HelloBazel* -r-xr-xr-x 1 ubuntu ubuntu 1173 Dec 18 12:22 HelloBazel.jar* -r-xr-xr-x 1 ubuntu ubuntu 990 Dec 18 12:22 HelloBazel.jar-2.params* -r-xr-xr-x 1 ubuntu ubuntu 96 Dec 18 12:22 HelloBazel.jar_manifest_proto* -r-xr-xr-x 1 ubuntu ubuntu 42 Dec 18 12:22 HelloBazel.jdeps* drwxrwxr-x 4 ubuntu ubuntu 4096 Dec 18 12:22 HelloBazel.runfiles/ -r-xr-xr-x 1 ubuntu ubuntu 121528 Dec 18 12:22 HelloBazel.runfiles_manifest* drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 18 12:22 _javac/
動作確認
java_binary()
ルールでビルドした Java アプリケーションを実行する方法はいくつかあります。
いずれも実行結果は変わりません(たぶん)。ここではもっとも簡単な bazel run
コマンドで実行してみましょう。
$ bazel run HelloBazel willard379 INFO: Found 1 target... Target //:HelloBazel up-to-date: bazel-bin/HelloBazel.jar bazel-bin/HelloBazel INFO: Elapsed time: 0.204s, Critical Path: 0.02s INFO: Running command line: bazel-bin/HelloBazel willard379 Hello, willard379!
上で少し説明しましたが、"HelloBazel" は "//:HelloBazel" を短縮したものです。その後に続く "willard379" は、Java の main()
メソッドに渡すコマンドライン引数です。
最後の行に main()
メソッドの実行結果 "Hello, willard379!" が出力されていますね。このように java_binary()
ルールでビルドしたものは bazel run
コマンドで起動することができます。
次は、Maven リポジトリ上に上がっているライブラリを使った依存性の追加をやってみましょう。
Java プロジェクトのビルド ~依存性の追加編~
Java ソースコード
HelloBazel.java
package willard379.bazel.sample; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; public class HelloBazel { public static void main(String[] args) { String name = (ArrayUtils.isNotEmpty(args) || StringUtils.isNotEmpty(args[0])) ? args[0] : "Bazel"; System.out.println(String.format("Hello, %s!", name)); } }
import 文に commons-lang の ArrayUtils と StringUtils を追加しています。 これらを使ってコマンドライン引数の存在チェックを行い、省略されていなければ指定された文字列を、省略されていたらデフォルト値 "Bazel" を使ってはろ~わ~るどするように変更しました。
WORKSPACE ファイル
WORKSPACE
maven_jar( name = "commons_lang3", artifact = "org.apache.commons:commons-lang3:3.5", )
WORKSPACE ファイルには、 Maven リポジトリ上の commons-lang3 への依存性を追加しています。
name
属性はこの依存性への識別子で、ワークスペース内でごっちゃにならなければ何でもよいです(たぶん)。
それなら artifactId をそのまま使えば良いっしょ!と考えてしまいますが、そこには大きな落とし穴があります(はいここ重要ですよ)。なんと maven_jar()
の name
属性にはハイフンが使えないのです! なので本来ならば commons-lang3
と書きたいところですが、ハイフンの代わりにアンダーバーを使って commons_lang3
としています。ここで指定した識別子は後で BUILD ファイルの方で使います。
artifact
属性には "<groupId>:<artifactId>:<version>" の書式、つまり Gradle と同じ書式を指定すれば良く、一番楽なのは Maven Central Repository のページからコピペしてくることです。
BUILD ファイル
BUILD
java_binary( name = "HelloBazel", srcs = glob(["src/main/java/**/*.java",]), main_class = "willard379.bazel.sample.HelloBazel", deps = ["@commons_lang3//jar",], )
deps
属性には、配列形式で「"@" + maven_jar()
の name
属性で指定した識別子 + "//jar"」を指定します。これによってコンパイル時にパスが通るようになります。
ビルドの実行と動作確認
前回のビルド結果が残っていますので、いったんキレイにしてから再ビルド、動作確認をしてみましょう。
$ bazel clean INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes. $ bazel run HelloBazel INFO: Found 1 target... Target //:HelloBazel up-to-date: bazel-bin/HelloBazel.jar bazel-bin/HelloBazel INFO: Elapsed time: 5.340s, Critical Path: 1.86s INFO: Running command line: bazel-bin/HelloBazel Hello, Bazel!
前回のビルド結果をキレイに消し去るには、 bazel clean
コマンドを実行します。
そういえば説明し忘れてましたが、 Bazel も Gradle と同様に、前回ビルド時と入力ファイルの差分がなければ、出力ファイルにも差分は発生しないはずと見做され、UP-TO-DATE としてビルドがスキップされます。
今回はソースファイルその他に差分があるので bazel clean
しなくても UP-TO-DATE にはなりませんが、気持ち的にフレッシュな気持ちで臨みたいのでクリーンしてみました。
でビルドの実行ですが、今回は bazel build
を省略しておもむろに bazel run
してみました。動作確認をするためには事前にビルドが必要なわけですが、 bazel run
の実行時にビルド結果がない場合は自動的にビルドが実行されます。ここら辺も Gradle と同じですね。
動作確認の実行結果ですが、コマンドライン引数を省略しているので、デフォルト値が使われて "Hello, Bazel!" と出力されていることが確認できます。ちゃんと commons-lang が使われていますね!
尚、bazel run
コマンド(または bazel-bin/HelloBazel シェル)で実行すると、commons-lang3-3.5.jar をクラスパスに通す設定をする必要もないので、java コマンドで実行するよりも楽ちんです!
次はいよいよ、ユニットテストを作成してみましょう。
Java プロジェクトのビルド ~ユニットテスト編~
構成はこんなカンジです。
Java ソースコード
HelloBazelTest.java
package willard379.bazel.sample; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import org.apache.commons.lang3.ArrayUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; public class HelloBazelTest { private ConsoleTestFixture console = new ConsoleTestFixture(); @Before public void setUp() { console.startMonitoring(); } @After public void tearDown() { console.stopMonitoring(); } @Test public void null空文字以外の文字列を渡すと_挨拶を返す() throws Exception { // set up fixture String[] args = { "willard379", }; // exercise SUT HelloBazel.main(args); // verify outcome assertThat(console.getStdout(), is("Hello, willard379!")); } @Test public void null配列を渡すと_デフォルトの挨拶を返す() throws Exception { // set up fixture String[] args = null; // exercise SUT HelloBazel.main(args); // verify outcome assertThat(console.getStdout(), is("Hello, Bazel!")); } // ry) }
HelloBazel.java に対するユニットテストを作成しました。HelloBazel.java の実行結果は標準出力に出力されるので、ConsoleTestFixture なるクラスを作って標準出力に出力された文字列をアサートできるようにしています。
このような「手続き上仕方がなくやらなければならない処理」というのはテストの本質ではないので、別クラスに追い出してテストケースクラスからは極力意識させないようにするに限ります。
ユニットテストは「どういう状況で、何を実行し、どうなっていれば合格か」だけが書かれているのが理想です。それ以外のあらゆる手続きはノイズです(と僕は考えます)。
今回はバグを見つけることが目的ではなく、ユニットテストを試すことが目的なので、テストケース足りてないのは問題なしとします。
ConsoleTestFixture.java
package willard379.bazel.sample; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import org.apache.commons.lang3.StringUtils; public class ConsoleTestFixture { private final PrintStream originalOut; private ByteArrayOutputStream mockStdout; public ConsoleTestFixture() { originalOut = System.out; } public void startMonitoring() { mockStdout = new ByteArrayOutputStream(); System.setOut(new PrintStream(mockStdout)); } public void stopMonitoring() { System.setOut(originalOut); mockStdout = null; } public String getStdout() { return StringUtils.chomp(new String(mockStdout.toByteArray(), StandardCharsets.UTF_8)); } }
テストケースクラスから追い出した処理です。
WORKSPACE ファイル
WORKSPACE
maven_jar( name = "commons_lang3", artifact = "org.apache.commons:commons-lang3:3.5", ) maven_jar( name = "junit", artifact = "junit:junit:4.12", ) maven_jar( name = "hamcrest_all", artifact = "org.hamcrest:hamcrest-all:1.3", )
JUnit と Hamcrest の依存関係を追加しました。
BUILD ファイル
BUILD
java_binary( name = "HelloBazel", srcs = glob(["src/main/java/**/*.java",]), main_class = "willard379.bazel.sample.HelloBazel", deps = ["@commons_lang3//jar",], ) java_library( name = "HelloBazelApi", srcs = glob(["src/main/java/**/*.java",]), deps = ["@commons_lang3//jar",], ) java_test( name = "HelloBazelTest", deps = [ ":HelloBazelApi", "@commons_lang3//jar", "@junit//jar", "@hamcrest_all//jar", ], srcs = glob(["src/test/java/**/*.java",]), test_class = "willard379.bazel.sample.HelloBazelTest", )
java_library()
ルールと java_test()
ルールを追加しました。
困ったことに、Bazel では java_binary()
ルールで定義されたプロジェクトのユニットテストはできません。
そのため、ほぼ同じ設定の java_library()
ルールを定義しています。識別子は "HelloBazelApi" としました。 java_binary()
で定義した場合と異なり bazel run
コマンドでの実行はできませんが、ユニットテストが目的なので問題ありません(定義が重複してしまっているのは誠に遺憾ですが)。
ユニットテストの設定は java_test
ルールで行っています。
deps
属性の依存性に、SUTである ":HelloBazelApi" とそれが依存する commons-lang3、テスティングフレームワークである JUnit と Hamcrest マッチャーを設定しています。
test_class
属性は、実行するテストケースクラスの FQCN です。Bazel では "*Test.java" のパターンを持つクラスをテストケースとみなして全て実行してくれるような親切心はありません(ぉ。実行するテストケースをきちんと指定してあげる必要があります(ここは改善を強く望むところ)。
ユニットテストの実行
実行は以下のコマンドで行います。
$ bazel clean INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes. $ bazel test HelloBazelTest INFO: Found 1 test target... Target //:HelloBazelTest up-to-date: bazel-bin/HelloBazelTest.jar bazel-bin/HelloBazelTest INFO: Elapsed time: 5.517s, Critical Path: 2.92s //:HelloBazelTest PASSED in 0.4s Executed 1 out of 1 test: 1 test passes. There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
やはり BUILD SUCCESSFUL が(ry
テストが成功したようです。テスト結果のレポートは bazel-testlogs/HelloBazelTest/ に出力されます。
$ ll bazel-testlogs/HelloBazelTest/ total 20 drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 18 13:41 ./ drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 18 13:41 ../ -r-xr-xr-x 1 ubuntu ubuntu 166 Dec 18 13:41 test.cache_status* -r-xr-xr-x 1 ubuntu ubuntu 358 Dec 18 13:41 test.log* -rw-r--r-- 1 ubuntu ubuntu 1209 Dec 18 13:41 test.xml
test.xml でテストケースごとの合否を確認することができます。
すべてのユニットテストをまとめて実行する
やっぱり java_test()
ルールの test_class
属性でテストケースクラスをいちいち指定しないといけないのはやってらんないので、すべてのテストをまとめて実行する方法を考えてみましょう。
TestSuite クラス
AllTests
package willard379.bazel.sample; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; import org.junit.extensions.cpsuite.ClasspathSuite.IncludeJars; import org.junit.runner.RunWith; @RunWith(ClasspathSuite.class) @ClassnameFilters({".*Test"}) @IncludeJars(true) public class AllTests {}
すべてのテストケースをまとめて実行するための TestSuite です。takari_cpsuite というライブラリを使っています。
BUILD ファイル
BUILD (デカくなってきたので差分のみ)
java_test( name = "AllTests", deps = [ ":HelloBazelApi", "@commons_lang3//jar", "@junit//jar", "@hamcrest_all//jar", ":takari_cpsuite", ], srcs = glob(["src/test/java/**/*.java",]), ) java_import( name = "takari_cpsuite", jars = [ "lib/takari-cpsuite-1.2.7.jar", ], )
今回は WORKSPACE ファイルに maven_jar()
ルールを追加するのではなく、プロジェクト内に jar ファイルを配置して、その jar を使ってくれるように設定してみました。
java_import()
ルールがその設定です。
jars
属性で jar ファイルを配置したパスを指定します。lib/takari-cpsuite-1.2.7.jar (ルートディレクトリからの相対パス) に配置しています。
java_test()
ルールでは、 deps
属性に先ほどの "takari_cpsuite" を追加したこと以外に、name
属性を "AllTests"とし、 test_class
属性を削除しました。
実は test_class
を省略した場合、 name
で指定した識別子と同じ名前を持つクラスが test_class
として使われます。
これによって test_class
に "AllTests" の FQCN を指定したのと同じ意味になります。
AllTests の実行
$ bazel clean INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes. $ bazel test AllTests INFO: Found 1 test target... Target //:AllTests up-to-date: bazel-bin/AllTests.jar bazel-bin/AllTests INFO: Elapsed time: 5.766s, Critical Path: 2.91s //:AllTests PASSED in 0.5s Executed 1 out of 1 test: 1 test passes.
java_test()
の name
を "AllTests" に変更したので、 bazel test
で指定する識別子も "AllTests" になります。
これですべてのユニットテストがまとめて実行されるようになりました。
スローテスト問題
Bazel には、時間のかかるテストケースを指定の時間でタイムアウトさせる機能が備わっています。
java_test()
ルールに size
属性を指定することで、サイズごとに決められた時間でユニットテストがタイムアウト(FAILURE)するようになります。
BUILD ファイル
BUILD (差分のみ)
java_test( name = "AllTests", size = "small", deps = [ ":HelloBazelApi", "@commons_lang3//jar", "@junit//jar", "@hamcrest_all//jar", ":takari_cpsuite", ], srcs = glob(["src/test/java/**/*.java",]), )
size
に指定できるのは以下の4種類で、タイムアウト以外にも RAM や CPU 数にも影響を与えるようです(詳細はここ と ここ を参照)。
size | タイムアウト |
---|---|
small | 60秒 |
medium | 300秒 |
large | 900秒 |
enormous | 3600秒 |
"small" を指定しているので、ユニットテストが60秒でタイムアウトするようになります。
Java ソースコード
SlowTest.java
package willard379.bazel.sample; import org.junit.Test; public class SlowTest { @Test public void _90秒かかるテスト() throws Exception { Thread.sleep(90 * 1_000); } }
ベタに90秒間スリープさせています。
ユニットテストの実行
$ bazel clean INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes. $ bazel test AllTests INFO: Found 1 test target... TIMEOUT: //:AllTests (see /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/HelloBazel/bazel-out/local-fastbuild/testlogs/AllTests/test.log). Target //:AllTests up-to-date: bazel-bin/AllTests.jar bazel-bin/AllTests INFO: Elapsed time: 65.251s, Critical Path: 62.47s //:AllTests TIMEOUT in 60.1s /home/ubuntu/.cache/bazel/_bazel_ubuntu/9ec7fed1ae4ad1d75540085b131e2506/execroot/HelloBazel/bazel-out/local-fastbuild/testlogs/AllTests/test.log Executed 1 out of 1 test: 1 fails locally.
狙い通り、テストがタイムアウトしました。
では、一時的にタイムアウト時間の上限を上げたくなった場合はどうしたらよいのでしょう?
ご安心ください。実行時オプションの --test_timeout
で秒数を指定することで、タイムアウト時間の変更が可能です。
$ bazel clean INFO: Starting clean (this may take a while). Consider using --expunge_async if the clean takes more than several minutes. $ bazel test AllTests --test_timeout=100 INFO: Found 1 test target... Target //:AllTests up-to-date: bazel-bin/AllTests.jar bazel-bin/AllTests INFO: Elapsed time: 95.190s, Critical Path: 92.90s //:AllTests PASSED in 90.5s Executed 1 out of 1 test: 1 test passes.
タイムアウト時間を100秒に設定したことで、ユニットテストが無事成功しました。
実は事前検証したとき、ここでちょっと困ったことが起きていました。 --test_timeout
実行時オプションを指定しただけでは入力ファイルに差分がないため、UP-TO-DATE となってユニットテストが実行されなかったのです。
上のように事前に bazel clean
を実行することでユニットテストが実行されるようになりましたが、命令実行後すこしの間画面を監視していないと、「ユニットテスト流してる間にコンビニでも行くか~! ⇒ コンビニから戻ったら UP-TO-DATE で空振り ⇒ orz」の極悪コンボの餌食になります。
終わりに
まだ Beta 版ということで至らない点は多々ありますし、JUnit 5 でのユニットテストや Travis CI との連携など試していないことがまだまだあって、手になじむようになるにはもう少し時間が必要そうですが、個人的には好きです。 Ant の build.xml や Maven の pom.xml よりも簡潔に記述できますし。自由度の観点では Gradle の build.gradle に及びませんが、その分属人的になりにくいため、誰が書いても大体同じものになるのではないかと思います。
とりあえす mvn archetype:generate
や gradle init
に相当する機能はぜひ実装していただきたいです。