Help us understand the problem. What is going on with this article?

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

TL;DR

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

はじめに

この記事は、先日開催した 機械学習 名古屋 第17回勉強会 での発表 Julia v1.0 の紹介 の補足資料的な立ち位置でもあります。

Julia_v1_0_の紹介_HackMD.jpg

この中で「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ファイル、自動生成されました。

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

[deps]
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 現在、テスト(の雛型)の追加は自動化されていない模様です。

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

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です。

FizzBuzzQuiz/test/runtests.jl
using FizzBuzzQuiz
using Test

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

↑ テストの雛型の例。
using FizzBuzzQuizusing 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) での実行例)。

FizzBuzzQuizTest01.png

ステップ2:テストのグループ化

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

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

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

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

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

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

↓実行結果

  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 ってナニモノ?

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

https://github.com/antimon2/FizzBuzzQuiz.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
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
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
FizzBuzzQuiz/test/runtests.jl
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で実装してみましょう!

参考


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

  2. この記事を書いたら Mojave にアップグレードしようかな…。 

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away