What is it, naokirin?

Juliaでテスト & モック

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_withrand(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