Juliaでのユニットテストの書き方と、やや踏み込んだコードを書いたときに重宝するモックについてを記載します。
バージョン
- Julia 1.5.3
- SimpleMock 1.2.0
Juliaでユニットテスト
実際の開発ではプロジェクトを作成して、プロジェクトへの追加が必要です。そちらは最後にして、まずはREPLで実行してみましょう。
Juliaでテストコードを書き始める場合は、Base.Testを利用します。stdlibなので、パッケージを追加せずに using Test
で開始できます。
$ julia _ _ _ _(_)_ | Documentation: https://docs.julialang.org (_) | (_) (_) | _ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help. | | | | | | |/ _` | | | | |_| | | | (_| | | Version 1.5.3 (2020-11-09) _/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release |__/ | julia> using Test
@test でテストする
それでは簡単にテストを実行してみます。
julia> @test 1 + 1 == 2 Test Passed
julia> @test 1 + 2 == 4 Test Failed at REPL[3]:1 Expression: 1 + 2 == 4 Evaluated: 3 == 4 ERROR: There was an error during testing
@test
をつけて実行した式がtrueならテストが通ります。一方でfalseの場合は失敗し、どの式が失敗したかと、評価結果が表示されます。
@testset を利用してテストケースをまとめる
REPLで試す分にはこれでも良いですが、プロジェクトで利用する場合は、もう少しテストをまとめてラベル付けし、テストセットにしたいかと思います。
Base.Testでは @testset
が用意されているので、そちらを利用しましょう。
julia> @testset "Math" begin @testset "Plus" begin @test 1 + 1 == 2 @test 1 + 2 == 3 end @testset "Minus" begin @test 1 - 1 == 0 @test 1 - 2 == 1 end end Minus: Test Failed at REPL[10]:9 Expression: 1 - 2 == 1 Evaluated: -1 == 1 Stacktrace: [1] top-level scope at REPL[49]:9 [2] top-level scope at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115 [3] top-level scope at REPL[49]:8 [4] top-level scope at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115 [5] top-level scope at REPL[49]:2 Test Summary: | Pass Fail Total Math | 3 1 4 Plus | 2 2 Minus | 1 1 2 ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.
あえて間違ったテストケースを混ぜましたが、上記のようにテストセットを利用することで多数のテストケースでもどこで間違えたかがわかりやすく表示されます。
@test_throws で例外の発生をテストする
@test_throws
を用いることで、例外の発生をテストできます。ここでは、ファイルオープンで存在しないファイルをオープンしようとしたときの SystemError の発生をテストしています。
julia> @test_throws SystemError open("not_found.txt") Test Passed Thrown: SystemError
その他のテスト
上記以外にもBase.Testではテストマクロが用意されています。詳しくはUnitTestingのドキュメントを参照してみてください。
Unit Testing · The Julia Language
@test_logs | ログ出力がされることをテストする |
@test_deprecated | Deprecatedが発生することをテストする |
@test_warn | 標準エラー出力に警告が出力されることをテストする |
@test_nowarn | 標準エラー出力に警告が出力されないことをテストする |
@test_broken | テストが失敗することをテストする |
@test_skip | テストをスキップする(Broken扱い) |
SimpleMock で関数をモックする
ここでSimpleMockを使ってモックを利用してみます。調べたところ、GitHub上でスターの多いJuliaでモック用ライブラリはMocking.jl と SimpleMock.jl がありますが、Mocking.jlのほうが古くからあり、SimpleMockのほうが比較的新しく作られたライブラリのようです。
ここでMocking.jlでなく、SimpleMock.jlを紹介したのには、大きく2つの理由があります。
- Mocking.jlでは、実装コード側の変更が必要になるが、SimpleMock.jlでは不要
- SimpleMock.jlでは、モックが呼び出されたことをテストできる
しかしながら、SimpleMock.jlのほうが動作はかなり遅いため、上記の制約が許せるならMocking.jlを使うほうがテスト実行速度が上がります(実際にSimpleMock.jlではIssueにパフォーマンスが挙がっているので、今後改善するかもしれません)。
それでは、SimpleMockを導入してみましょう。
julia> # ここで `]` を押してPkg modeへ (@v1.5) pkg> add "SimpleMock" Resolving package versions... Installed SimpleMock ─ v1.2.0 Installed Cassette ─── v0.3.3 Updating `~/.julia/environments/v1.5/Project.toml` [a896ed2c] + SimpleMock v1.2.0 Updating `~/.julia/environments/v1.5/Manifest.toml` [7057c7e9] + Cassette v0.3.3 [a896ed2c] + SimpleMock v1.2.0 (@v1.5) pkg > # backspace を押して julian mode に戻る
それでは実際に、SimpleMockを試します。まずはテスト対象の関数を用意します。
julia> # 1以上max以下の乱数を生成する関数 julia> function myrand(max::Integer) rand(1:max) end
乱数による関数を用意しました。この関数で呼び出されているrandをSimpleMockでモックにして関数をテストみます。
julia> using SimpleMock julia> mock((rand, Any) => Mock((rng) -> 1)) do rand @test myrand(10) == 1 @test called_once_with(rand, 1:10) # rand(1:10) が一度呼び出されることをテスト end Test Passed
mock
関数により、モックする関数、渡される引数の型、モックの挙動を定義し、ブロック内でテストを行います。テスト内では、モックが適用されるため、 rand
は常に1を返すようになっているためテストに成功します。
また、 called_once_with
で rand(1:10)
が呼び出されていることをテストしています。
SimpleMockはドキュメントが揃っていませんが、よく使いそうな関数のみ、以下に書いておきます。
called_with | 回数、呼び出しタイミング等の限定なく、指定された引数で呼ばれたかどうか |
called_last_with | 最後に指定された引数で呼び出されたか |
called | 単に関数が呼び出されたか(引数の指定なし) |
ncalls | 関数が何回呼び出されたか |
プロジェクトにテストを導入する
それではプロジェクトにテストを導入してみましょう。ちなみにここでの手順はJulia v1.2以降のものになります。
プロジェクトを作成する
すでに作成済みのプロジェクトに導入する場合はこのステップは飛ばしてください。
(@v1.5) pkg> generate TestProject.jl
Pkg mode で generate
コマンドを利用することで、現在のディレクトリに新規プロジェクトが作成されます。
ちなみに現在のディレクトリの確認は pwd
、 移動は cd
関数をJulian modeで使うと可能です。
# 現在のディレクトリを確認 julia> pwd() # /julia_projects のディレクトリに移動 julia> cd("/julia_projects")
プロジェクトにテストコードを用意する
まずは普通にプロジェクト以下にtestディレクトリを作成します。
$ mkdir TestProject.jl/test
次に、テストの起点になる runtests.jl
を用意します。今回は、 helloworld.jl
をincludeするようにしています。
$ cat TestProject.jl/test/runtests.jl using Test @testset "HelloWorld" begin include("helloworld.jl") end $ cat TestProject.jl/test/helloworld.jl using Test @testset "HelloWorld" begin include("helloworld.jl") end root@780dd445de4e:/TestProject# cat test/helloworld.jl using Test @test "hello" * "world" == "helloworld"
テスト実行のためのパッケージを追加する
ここですぐにテストを実行したいところですが、プロジェクトにTestパッケージが追加されていないため、このまま実行してもエラーになります。
REPLに戻り、Pkg mode に入ります。そして、activate TestProject.jl/test
でtestディレクトリをアクティベートします(この時、現在のディレクトリの位置に注意。ここではTestProject.jlの親ディレクトリにいるままを想定)。
(@v1.5) pkg> activate TestProject.jl/test Activating environment at `/TestProject/test/Project.toml` (test) pkg>
ここでテストに必要なパッケージを追加します。
(test) pkg> add Test
テストを実行する
テストを実行してみます。testディレクトリではなく、プロジェクトの方をactivateして、testを実行します。
(test) pkg> activate TestProject.jl Activating environment at `/TestProject/Project.toml` (TestProject) pkg> test Testing TestProject Status `/tmp/jl_pW5c9L/Project.toml` [4f0e8706] TestProject v0.1.0 `/TestProject` [8dfed614] Test Status `/tmp/jl_pW5c9L/Manifest.toml` [4f0e8706] TestProject v0.1.0 `/TestProject` [2a0f44e3] Base64 [8ba89e20] Distributed [b77e0a4c] InteractiveUtils [56ddb016] Logging [d6f4376e] Markdown [9a3f8284] Random [9e88b42a] Serialization [6462fe0b] Sockets [8dfed614] Test Test Summary: | Pass Total HelloWorld | 1 1 Testing TestProject tests passed
シェルのコマンドから実行する場合は以下で実行しましょう。
$ julia --project -e 'using Pkg;Pkg.test()'
終わりに
Juliaではプロジェクト作成時にすぐはテストが入っていないので、最初に自力で導入するいく必要があるのは少し手間ですね。。。
また今回調べた結果として、特定のテストセットのみを実行するなどの機能は見当たらず、今のところは自力で対応するのが良さそうです。
ファイル単位で引数指定に対応できるようにテストコードで対応しているライブラリがあったので参考として記載しておきます。
StaticArrays.jl/runtests.jl at master · JuliaArrays/StaticArrays.jl · GitHub