TL;DR
(v1.0) pkg> generate HOGE
-
test/runtests.jl
Using Test
julia --project -e 'using Pkg;Pkg.test()'
はじめに
この記事は、先日開催した 機械学習 名古屋 第17回勉強会 での発表 Julia v1.0 の紹介 の補足資料的な立ち位置でもあります。
この中で「Julia の仮想環境」の話と「Julia で プロジェクト開発」という話を、実際にデモをしながらプレゼンした(しようとした)のですが、時間が足りなかったり途中ブラウザがフリーズしたりで十分な説明ができなかったのでした。
ので、特に今回の発表の目玉だった「プロジェクト開発」、その中でも特に「ユニットテスト(っぽいもの)」について、実例を含めて少し詳しく解説しよう、というのがこの記事になります1。
環境等
- Julia v1.0
- Julia v0.7 でも同様
- 動作確認環境:
- macOS 10.13.6 High Sierra2
- ubuntu 14.04 / 16.04 LTS / 18.04 LTS
- Windows 10 Professional
ステップ0:プロジェクト作成
ユニットテストが必要になる、ってことは、パッケージやアプリを Julia で開発しようとしている、と言うことだと思います。
だったら、まずは Julia のプロジェクトを作成しましょう。
プロジェクトは、Julia の Pkg REPL-mode で generate
コマンドで作成できます。
(v1.0) pkg> generate FizzBuzzQuiz
Generating project FizzBuzzQuiz:
FizzBuzzQuiz/Project.toml
FizzBuzzQuiz/src/FizzBuzzQuiz.jl
(v1.0) pkg> activate FizzBuzzQuiz
(FizzBuzzQuiz) pkg>
generate
コマンドを実行すると、カレントディレクトリに FizzBuzzQuiz
サブディレクトリが作られ、その中に1ファイル、さらにそのサブディレクトリに1ファイル、自動生成されました。
name = "FizzBuzzQuiz"
uuid = "21722cf0-b7d5-11e8-334b-05128eaae1be"
authors = ["antimon2 <antimon2.me@gmail.com>"]
version = "0.1.0"
[deps]
module FizzBuzzQuiz
greet() = print("Hello World!")
end # module
仮想環境を作ったときには、何かパッケージを add
しないと作成されなかった Project.toml が、プロジェクト名や作成者情報などの最低限の初期情報とともに作成されています(まだ何も add
していないので [deps]
エントリーは空です)。
あと src
ディレクトリに FizzBuzzQuiz.jl
というファイルが作成されて、"Hello World!"
と表示するだけのデフォルト実装がされていますね。
このファイルを編集してパッケージ(なりアプリなり)を構築していく、というわけです。
ここまでの機能は、Julia 標準で用意してくれています。便利ですね!
ステップ1:テストの追加
続いてテストを追加していきましょう。
比較的簡単に追加することが出来ます。
ただし 2018/10/08 現在、テスト(の雛型)の追加は自動化されていない模様です。
取り敢えず、以下に従ってファイルを編集・記述すれば、テストの追加が出来ます。
name = "FizzBuzzQuiz"
uuid = "21722cf0-b7d5-11e8-334b-05128eaae1be"
authors = ["antimon2 <antimon2.me@gmail.com>"]
version = "0.1.0"
[deps]
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
↑ [extras]
以降の行を追記。定型なのでそのままコピペでOKです。
using FizzBuzzQuiz
using Test
@test 1 + 1 == 2
# 作成中のモジュールのテストを書く
# : 《以下略》
↑ テストの雛型の例。
using FizzBuzzQuiz
、using Test
の記述の後で、 @test 〜
マクロを使ってテストを記述していけばOK。取り敢えず「絶対成功するテスト」を1件だけ記述してみました。
これを実行するには、以下のようにします3:
$ pwd
/path/to/FizzBuzzQuiz
$ julia --project
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.0.1 (2018-09-29)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> # ']' キーで Pkg REPL-mode に移行
(FizzBuzzQuiz) pkg>test
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Testing FizzBuzzQuiz tests passed
はい、Pkg REPL-mode で test
コマンドを引数無しで実行するだけ! 簡単ですね!
ちなみにコマンドラインでワンラインでも実行可能です。
$ pwd
/path/to/FizzBuzzQuiz
$ julia --project -e 'using Pkg;Pkg.test()'
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Testing FizzBuzzQuiz tests passed
いちいち using Pkg;Pkg.〜
としなければならないのが微妙にめんどくさいですが、とにかくコマンドラインからテストを実行できます。
これでTDDもできますね!4
ちなみに --color=yes
オプションも付けると Pkg REPL-mode での実行時と同様にカラーリングされます(Linux (on vagrant) での実行例)。
ステップ2:テストのグループ化
とりあえず runtests.jl
にどんどん @test 〜
マクロでテストを追加していけば、ユニットテストっぽいものは実行できます。
もう一工夫して、@testset
マクロを利用してテストをグループ化しましょう。
using FizzBuzzQuiz
using Test
@testset "Addition" begin
@test 1 + 1 == 2
@test 2 + 3 == 5
end
@testset "Multiplication" begin
@test 1 * 1 == 1
@test 2 * 3 == 6
end
コレを実行すると、以下のような出力になります。
FizzBuzzQuiz) pkg> test
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `/path/to/FizzBuzzQuiz/Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Test Summary: | Pass Total
Addition | 2 2
Test Summary: | Pass Total
Multiplication | 2 2
Testing FizzBuzzQuiz tests passed
設定したグループごとに、テストサマリが表示されていますね。わかりやすいですね!
もちろん失敗するテストを書けばその情報も表示されます。
(FizzBuzzQuiz) pkg> test
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `/path/to/FizzBuzzQuiz/Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Test Summary: | Pass Total
Addition | 2 2
Multiplication: Test Failed at /path/to/FizzBuzzQuiz/test/runtests.jl:11
Expression: 2 * 3 == 5
Evaluated: 6 == 5
Stacktrace:
: 《中略》
Test Summary: | Pass Fail Total
Multiplication | 1 1 2
ERROR: LoadError: Some tests did not pass: 1 passed, 1 failed, 0 errored, 0 broken.
in expression starting at /path/to/FizzBuzzQuiz/test/runtests.jl:9
ERROR: Package FizzBuzzQuiz errored during testing
煩雑になるので省略しちゃいましたが、テストに失敗すると Test.FallbackTestSetException
という例外が throw され、そのスタックトレースも表示されます。
不具合の特定には有用ですけれどね。
ただ、サマリが分断されてちょっと見にくいですよね。
これをちょっとだけマシにする方法があります。@testset
を入れ子にする方法です。
using FizzBuzzQuiz
using Test
@testset "Calcuration" begin
@testset "Addition" begin
@test 1 + 1 == 2
@test 2 + 3 == 5
end
@testset "Multiplication" begin
@test 1 * 1 == 1
@test 2 * 3 == 5
end
end
↓実行結果
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `/path/to/FizzBuzzQuiz/Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Multiplication: Test Failed at /path/to/FizzBuzzQuiz/test/runtests.jl:12
Expression: 2 * 3 == 5
Evaluated: 6 == 5
Stacktrace:
: 《中略》
Test Summary: | Pass Fail Total
Calcuration | 3 1 4
Addition | 2 2
Multiplication | 1 1 2
ERROR: LoadError: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.
in expression starting at /path/to/FizzBuzzQuiz/test/runtests.jl:4
ERROR: Package FizzBuzzQuiz errored during testing
サマリが大分類でまとめて表示されるし、エラーの起きた箇所を知りたければその上にでているので分かりやすいですね!
使い途としては色々ありますが、「ユニットテスト(ホワイトボックステスト)」を意識するなら以下のような入れ子構造なんかどうでしょう?
- 外側:
@testset 《モジュール》
- 内側:
@testset 《関数と引数の組合せ》
- 内部:
@test 《テストを記述》
- 内部:
- 内側:
もしくは、Ruby の RSpec みたいな考え方・使い方も出来ますね。
- 外側:
@testset 《モジュール、型等》
- 内側:
@testset 《仕様を自然言語で記述》
- 内部:
@test 《テストを記述》
- 内部:
- 内側:
なお、さらに多段に入れ子にすることも可能です。
用意されているテストの種類
@test 〜
以外にもいくつかテスト用マクロが用意されています。以下に表で簡単にまとめます。
マクロ | 使い途 |
---|---|
@test |
結果が true かどうか |
@test_throws |
指定したエラー(例外)が throw されるかどうか |
@test_warn |
標準エラー出力に指定した警告が出力されるかどうか |
@test_nowarn |
標準エラー出力に警告が出力されないかどうか |
@test_logs |
指定した種類と内容のログが出力されるかどうか |
@test_deprecated |
(指定した書式の)Deprecated 警告が発生するかどうか |
@test_broken |
テストが失敗するかどうか |
@test_skip |
テストをスキップする(broken 扱いにする) |
詳細は、公式ドキュメントの Test のページ 見るなり Julia の REPL で ?@test_xxx
するなりして確認してみてください。
余談
DocTest みたいなのないの?
他言語だと、DocString にテストコードを埋め込んで、それを自動実行する方法もあります。
Julia にも現段階で、DocTest を埋め込む書式は用意されています。
module HOGE
"""
greet()
# Sample
```jldoctest
julia> using HOGE; HOGE.greet()
"Hello World!"
``` """
greet() = "Hello World!"
end
ただし、この方法で埋め込んだ DocTest をユニットテストとして 実行する手段が現在の所存在しません。
Documenter.jl
という外部パッケージを導入すれば、makedocs
実行時に doctest を抽出して実行してくれますが、同時にドキュメントも生成されてしまう(ので無駄に時間がかかる) 上に テストエラーが起きてもそれを捕捉する手段がない(ドキュメント生成時の出力を目視するしかない)という、DocTest としては使えない状況です。
今後、Documenter.jl
もしくは関連ツールの拡充を待つか、他の外部パッケージでの対応を待つしか現状としてはなさそうです。
そもそもサンプルの FizzBuzzQuiz
ってナニモノ?
最終成果は↓に上げときました。
元ネタはこちらです:
改めてテストコードを全部晒します:
@testset "Fizz & Buzz" begin
# @test fizz(1) == 1
@test 1 |> fizz == 1
@test 3 |> fizz == "Fizz"
@test 1 |> buzz == 1
@test 5 |> buzz == "Buzz"
# @test buzz(fizz(1)) == 1
@test 1 |> fizz |> buzz == 1
@test 3 |> fizz |> buzz == "Fizz"
@test 5 |> fizz |> buzz == "Buzz"
@test 15 |> fizz |> buzz == "FizzBuzz"
@test 15 |> buzz |> fizz == "BuzzFizz"
end
@testset "Pezz" begin
# @test pezz(buzz(fizz(7))) == "Pezz"
@test 7 |> fizz |> buzz |> pezz == "Pezz"
@test 21 |> fizz |> buzz |> pezz == "FizzPezz"
@test 35 |> fizz |> buzz |> pezz == "BuzzPezz"
@test 105 |> fizz |> buzz |> pezz == "FizzBuzzPezz"
@test 105 |> fizz |> pezz |> buzz == "FizzPezzBuzz"
@test 105 |> pezz |> buzz |> fizz == "PezzBuzzFizz"
# 以下も念のため
@test 1 |> fizz |> buzz |> pezz == 1
@test 3 |> fizz |> buzz |> pezz == "Fizz"
@test 5 |> fizz |> buzz |> pezz == "Buzz"
@test 15 |> fizz |> buzz |> pezz == "FizzBuzz"
@test 15 |> buzz |> fizz |> pezz == "BuzzFizz"
@test 104 |> fizz |> buzz |> pezz == 104
end
@testset "Hozz" begin
@test 13 |> fizz |> buzz |> hozz == "Aho"
@test 3 |> fizz |> buzz |> hozz == "FizzAho"
@test 35 |> fizz |> buzz |> hozz == "BuzzAho"
@test 30 |> fizz |> buzz |> hozz == "FizzBuzzAho"
@test 30 |> hozz |> buzz |> fizz == "AhoBuzzFizz"
end
using FizzBuzzQuiz
using Test
tests = ["fizz_buzz", "pezz", "hozz"]
for test = tests
# println("testing $test.jl...")
include("$test.jl")
end
出力結果:
$ julia --project -e 'using Pkg;Pkg.test()'
Updating registry at `~/.julia/registries/General`
Updating git-repo `https://github.com/JuliaRegistries/General.git`
Resolving package versions...
Updating `path/to/FizzBuzzQuiz/Project.toml`
[no changes]
Testing FizzBuzzQuiz
Resolving package versions...
Test Summary: | Pass Total
Fizz & Buzz | 9 9
Test Summary: | Pass Total
Pezz | 12 12
Test Summary: | Pass Total
Hozz | 5 5
Testing FizzBuzzQuiz tests passed
要するに「入力は(初めは)整数値だが、関数を通るごとに整数値または文字列になっていくような FizzBuzz をどう書いたら良いか」という問題、および「例えば Julia ならこう書けるよ、どうしてこれでうまく動くのか分かるかな?」という問題。
実装例は、前回記事 および リポジトリのソース を見てみてください。
もしくは、実装例を見ずにこのテストケースだけお手元に用意して、自分でTDDで実装してみましょう!
参考
- Julia 1.0 Documentation
-
Documenter.jl(リポジトリ)
- Documenter.jl(開発版ドキュメント)
-
本当はその前の「仮想環境管理」からがっつり解説しようと思っていたのですが、ちょうどおあつらえ向きの記事(=Julia v1.0.0で入った仮想環境の管理について by @YuK_Ota さん)が先日公開されたので、そこまでの説明はもう不要かな、と。 ↩
-
この記事を書いたら Mojave にアップグレードしようかな…。 ↩
-
Project.toml が存在するディレクトリで
julia --project
と--project
オプションを付けて起動すると、そのプロジェクト(もしくは仮想環境)を activate した状態で起動します。これ地味に便利。Python のpipenv run 〜
とか、Ruby ならbundle exec 〜
に相当。 ↩ -
ちなみにコマンドラインから実行した場合、終了ステータスがきちんと「成功時:0」「失敗時:1(!=0)」となるので、シェルスクリプトやCI等で「テストが成功しないと次に進めない」などといった処理も記述可能です。 ↩
-
実はこの記事は「過去に Qiita で書いた Julia 記事のコードを最新 v1.0 で動くものにリライトしよう」という超個人プロジェクトの第3弾にもなっています。今回は Julia v0.5.x / v0.6.x から v1.0.x へのポーティング例です。 ↩