Julia v1.0 でユニットテスト

# TL;DR

+ `(v1.0) pkg> generate HOGE`
+ `test/runtests.jl`
    + `Using Test`
+ `julia --project -e 'using Pkg;Pkg.test()'`

# はじめに

この記事は、先日開催した [機械学習 名古屋 第17回勉強会](https://machine-learning.connpass.com/event/94992/) での発表 [Julia v1.0 の紹介](https://hackmd.io/p/HyHP1am_m) の補足資料的な立ち位置でもあります。

[![Julia_v1_0_の紹介_HackMD.jpg](https://qiita-image-store.s3.amazonaws.com/0/30400/db6ad180-a43b-56cd-7d2c-bb4d7349304e.jpeg)](https://hackmd.io/p/HyHP1am_m)

この中で「Julia の仮想環境」の話と「Julia で プロジェクト開発」という話を、実際にデモをしながらプレゼンした（しようとした）のですが、時間が足りなかったり途中ブラウザがフリーズしたりで十分な説明ができなかったのでした。
ので、特に今回の発表の目玉だった「プロジェクト開発」、その中でも特に「ユニットテスト（っぽいもの）」について、実例を含めて少し詳しく解説しよう、というのがこの記事になります[^julia_env_article]。

[^julia_env_article]: 本当はその前の「仮想環境管理」からがっつり解説しようと思っていたのですが、ちょうどおあつらえ向きの記事（＝[Julia v1.0.0で入った仮想環境の管理について](https://qiita.com/YuK_Ota/items/15780a61fb7aa4a49de6) by @YuK_Ota さん）が先日公開されたので、そこまでの説明はもう不要かな、と。


## 環境等

+ Julia v1.0
    + Julia v0.7 でも同様
+ 動作確認環境：
    + macOS 10.13.6 High Sierra[^to_mojave]
    + ubuntu 14.04 / 16.04 LTS / 18.04 LTS
    + Windows 10 Professional

[^to_mojave]: この記事を書いたら Mojave にアップグレードしようかな…。


# ステップ0：プロジェクト作成

ユニットテストが必要になる、ってことは、パッケージやアプリを Julia で開発しようとしている、と言うことだと思います。
だったら、まずは Julia のプロジェクトを作成しましょう。

プロジェクトは、Julia の Pkg REPL-mode で `generate` コマンドで作成できます。

```jl
(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ファイル、自動生成されました。

```toml:FizzBuzzQuiz/Project.toml
name = "FizzBuzzQuiz"
uuid = "21722cf0-b7d5-11e8-334b-05128eaae1be"
authors = ["antimon2 <antimon2.me@gmail.com>"]
version = "0.1.0"

[deps]
```

```jl:FizzBuzzQuiz/src/FizzBuzzQuiz.jl
module FizzBuzzQuiz

greet() = print("Hello World!")

end # module
```

仮想環境を作ったときには、何かパッケージを `add` しないと作成されなかった **Project.toml** が、プロジェクト名や作成者情報などの最低限の初期情報とともに作成されています（まだ何も `add` していないので `[deps]` エントリーは空です）。
あと `src` ディレクトリに `FizzBuzzQuiz.jl` というファイルが作成されて、`"Hello World!"` と表示するだけのデフォルト実装がされていますね。
このファイルを編集してパッケージ（なりアプリなり）を構築していく、というわけです。

ここまでの機能は、Julia 標準で用意してくれています。便利ですね！


# ステップ1：テストの追加

続いてテストを追加していきましょう。
比較的簡単に追加することが出来ます。

ただし 2018/10/08 現在、テスト（の雛型）の追加は自動化されていない模様です。

取り敢えず、以下に従ってファイルを編集・記述すれば、テストの追加が出来ます。

```toml:FizzBuzzQuiz/Project.toml
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です。

```jl:FizzBuzzQuiz/test/runtests.jl
using FizzBuzzQuiz
using Test

@test 1 + 1 == 2
# 作成中のモジュールのテストを書く
#  : 《以下略》
```

↑ テストの雛型の例。
`using FizzBuzzQuiz`、`using Test` の記述の後で、 `@test 〜` マクロを使ってテストを記述していけばOK。取り敢えず「絶対成功するテスト」を1件だけ記述してみました。

これを実行するには、以下のようにします[^julia__project]：

[^julia__project]: Project.toml が存在するディレクトリで `julia --project` と `--project` オプションを付けて起動すると、そのプロジェクト（もしくは仮想環境）を activate した状態で起動します。これ地味に便利。Python の `pipenv run 〜` とか、Ruby なら `bundle exec 〜` に相当。

```bash
$ 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` コマンドを引数無しで実行するだけ！　簡単ですね！


ちなみにコマンドラインでワンラインでも実行可能です。

```bash
$ 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もできますね！[^exit_status]

[^exit_status]: ちなみにコマンドラインから実行した場合、終了ステータスがきちんと「成功時：0」「失敗時：1（!=0）」となるので、シェルスクリプトやCI等で「テストが成功しないと次に進めない」などといった処理も記述可能です。

ちなみに `--color=yes` オプションも付けると Pkg REPL-mode での実行時と同様にカラーリングされます（Linux (on vagrant) での実行例）。

![FizzBuzzQuizTest01.png](https://qiita-image-store.s3.amazonaws.com/0/30400/ee54d195-1558-a609-d7d7-f34db7d91ebb.png)


# ステップ2：テストのグループ化

とりあえず `runtests.jl` にどんどん `@test 〜` マクロでテストを追加していけば、ユニットテストっぽいものは実行できます。

もう一工夫して、`@testset` マクロを利用してテストをグループ化しましょう。

```jl:FizzBuzzQuiz/test/runtests.jl
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
```

コレを実行すると、以下のような出力になります。

```jl
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

```

設定したグループごとに、テストサマリが表示されていますね。わかりやすいですね！

もちろん失敗するテストを書けばその情報も表示されます。

```jl
(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` を入れ子にする方法です。


```jl:FizzBuzzQuiz/test/runtests.jl
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
```

↓実行結果

```jl
  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 のページ](https://docs.julialang.org/en/v1/stdlib/Test/) 見るなり Julia の REPL で `?@test_xxx` するなりして確認してみてください。


# 余談

## DocTest みたいなのないの？

他言語だと、DocString にテストコードを埋め込んで、それを自動実行する方法もあります。

Julia にも現段階で、DocTest を埋め込む書式は用意されています。

```julia
module HOGE

"""
greet()

# Sample

```jldoctest
julia> using HOGE; HOGE.greet()
"Hello World!"

``` """
greet() = "Hello World!"

end
```

ただし、この方法で埋め込んだ DocTest をユニットテストとして **実行する手段が現在の所存在しません**。

[`Documenter.jl`](https://github.com/JuliaDocs/Documenter.jl) という外部パッケージを導入すれば、`makedocs` 実行時に doctest を抽出して実行してくれますが、*同時にドキュメントも生成されてしまう（ので無駄に時間がかかる）* 上に *テストエラーが起きてもそれを捕捉する手段がない*（ドキュメント生成時の出力を目視するしかない）という、DocTest としては使えない状況です。

今後、`Documenter.jl` もしくは関連ツールの拡充を待つか、他の外部パッケージでの対応を待つしか現状としてはなさそうです。


## そもそもサンプルの `FizzBuzzQuiz` ってナニモノ？

最終成果は↓に上げときました。

https://github.com/antimon2/FizzBuzzQuiz.jl

元ネタはこちらです：

+ [「FizzBuzzクイズ」クイズ－Ruby編](https://qiita.com/sumim/items/f26902f2b813c075a619) by @sumim
    + [「FizzBuzzクイズ」クイズ with #julialang](https://qiita.com/antimon2/items/480c76d255d074eeb3cc) by @antimon2[^jl_06_2_10]

[^jl_06_2_10]: 実はこの記事は「過去に Qiita で書いた Julia 記事のコードを最新 v1.0 で動くものにリライトしよう」という超個人プロジェクトの第3弾にもなっています。今回は Julia v0.5.x / v0.6.x から v1.0.x へのポーティング例です。

改めてテストコードを全部晒します：

```jl:FizzBuzzQuiz/test/fizz_buzz.jl
@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
```

```jl:FizzBuzzQuiz/test/pezz.jl
@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
```

```jl:FizzBuzzQuiz/test/hozz.jl
@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
```

```jl:FizzBuzzQuiz/test/runtests.jl
using FizzBuzzQuiz
using Test

tests = ["fizz_buzz", "pezz", "hozz"]
for test = tests
    # println("testing $test.jl...")
    include("$test.jl")
end
```

出力結果：

```bash
$ 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 ならこう書けるよ、どうしてこれでうまく動くのか分かるかな？」という問題。

実装例は、[前回記事](https://qiita.com/antimon2/items/480c76d255d074eeb3cc) および [リポジトリのソース](https://github.com/antimon2/FizzBuzzQuiz.jl/blob/master/src/FizzBuzzQuiz.jl) を見てみてください。
もしくは、実装例を見ずにこのテストケースだけお手元に用意して、自分でTDDで実装してみましょう！


# 参考

+ [Julia 1.0 Documentation](https://docs.julialang.org/en/v1/)
    + [Unit Testing](https://docs.julialang.org/en/v1/stdlib/Test/)
    + [Adding tests to the package](https://docs.julialang.org/en/v1/stdlib/Pkg/#Adding-tests-to-the-package-1) - [Pkg](https://docs.julialang.org/en/v1/stdlib/Pkg/)
    + [Documentation](https://docs.julialang.org/en/v1/manual/documentation/)
+ [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl)（リポジトリ）
    + [Documenter.jl](https://juliadocs.github.io/Documenter.jl/latest/)（開発版ドキュメント）