Test
Julia の基礎部分のテスト
Julia の開発は高速に進んでおり、複数のプラットフォームで機能を検証する幅広いテストスイートを持ちます。このテストスイートは Julia をソースからビルドしたときは make test
で実行でき、バイナリインストールしたときは Base.runtests()
で実行できます。
Base.runtests
── 関数
Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
exit_on_error=false, revise=false, [seed])
ncores
個のプロセッサを使って tests
が指定する Julia のユニットテストを実行します。tests
は文字列または文字列の配列です。exit_on_error
が false
だといずれかのテストが失敗した場合でも他のファイルのテストは実行され、true
だとテストが一つ失敗した時点で全ての実行が終了します。revise
が true
だと、テストを実行する前に Base
および標準ライブラリに対する変更が Revise
パッケージを使って読み込まれます。キーワード引数 seed
が与えられると、テストを実行するコンテキストにおけるグローバルの RNG がその値でシードされます。seed
が与えなければシードはランダムに選ばれます。
基礎的なユニットテスト
Test
モジュールは簡単なユニットテストの機能を提供します。ユニットテストとは書いたコードが予想通りの結果を返すことを確認するコードのことです。開発中に完成したコードが持つべき振る舞いを書き留めたり、将来コードを変更した後にも正しく動作することを保証するために利用します。
簡単なユニットテストは @test
マクロと @test_throws
マクロで行います:
Test.@test
── マクロ
@test ex
@test f(args...) key=val ...
式 ex
が true
に評価されることをテストします。ex
の評価結果が true
なら Pass
型の値、false
なら Fail
型の値、評価できなかったときは Error
型の値を返します (Pass
, Fail
, Error
は Result
の部分型です)。
例
julia> @test true
Test Passed
julia> @test [1, 2] + [2, 1] == [3, 3]
Test Passed
@test f(args...) key=val...
の形式は @test f(args..., key=val...)
と等価であり、近似比較のような中置記法の構文を持つ関数の呼び出しをテストするときに有用です:
julia> @test π ≈ 3.14 atol=0.01
Test Passed
これは @test ≈(π, 3.14, atol=0.01)
とも書けますが、読みにくくなります。二つ目以降の式に代入文 (k=v
) でない式を渡すとエラーになります。
Test.@test_throws
── マクロ
@test_throws exception expr
式 expr
が例外 exception
を送出することをテストします。例外は型もしくは値で指定でき、値で指定するとフィールドの比較で等価性が判定されます。@test_throws
は末尾にキーワード引数を取る形式をサポートしないことに注意してください。
例
julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
Thrown: BoundsError
julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Test Passed
Thrown: DimensionMismatch
例えば、次の関数 foo(x)
が予想通りに動作することを確認したいとします:
julia> using Test
julia> foo(x) = length(x)^2
foo (generic function with 1 method)
このときは @test
を使います。このマクロに渡した条件が true
なら Pass
オブジェクトが返ります:
julia> @test foo("bar") == 9
Test Passed
julia> @test foo("fizz") >= 10
Test Passed
条件が false
だと Fail
オブジェクトが返り、例外が送出されます:
julia> @test foo("f") == 20
Test Failed at none:1
Expression: foo("f") == 20
Evaluated: 1 == 20
ERROR: There was an error during testing
例外が送出されたために条件が評価できなかった場合は Error
オブジェクトが返ります。今の例では length
がシンボルに対して定義されていないので、foo
にシンボルを渡すと例外が送出されます:
julia> @test foo(:cat) == 1
Error During Test
Test threw an exception of type MethodError
Expression: foo(:cat) == 1
MethodError: no method matching length(::Symbol)
Closest candidates are:
length(::SimpleVector) at essentials.jl:256
length(::Base.MethodList) at reflection.jl:521
length(::MethodTable) at reflection.jl:597
...
Stacktrace:
[...]
ERROR: There was an error during testing
式の評価で例外が送出されるべきなら、そのことは @test_throws
を使って確認できます:
julia> @test_throws MethodError foo(:cat)
Test Passed
Thrown: MethodError
テストセットの利用
関数が様々な入力に対して正しく動作することの確認には通常たくさんのテストが使われます。テストが失敗した場合のデフォルトの振る舞いはすぐに例外を送出するというものですが、普通は残りのテストを全て実行してテストされているコードに存在するエラーの全体像を把握した方が望ましいでしょう。
@testset
マクロを使うと、いくつかのテストをテストセットとしてグループ化できます。テストセットに含まれるテストはまとめて実行され、結果はテストセットの終わりにまとめて出力されます。いずれかのテストが失敗あるいはエラーを送出すると、テストセットに含まれるテストが全て実行されてから TestSetException
が送出されます。
Test.@testset
── マクロ
@testset [CustomTestSet] [option=val ...] ["description"] begin ... end
@testset [CustomTestSet] [option=val ...] ["description $v"] for v in (...) ... end
@testset [CustomTestSet] [option=val ...] ["description $v, $w"] for v in (...), w in (...) ... end
新しいテストセットを開始します。for
ループが与えられた場合は反復ごとに新しいテストセットを開始します。
独自のテストセット型が CustomTestSet
に与えられなければデフォルトの DefaultTestSet
が使われます。DefaultTestSet
は全てのテスト結果を記録してテストセットの終了時にテスト結果を出力し、もし Fail
や Error
があればトップレベルの (入れ子になっていない) テストセットが終わるときに例外を送出します。
CustomTestSet
には独自のテストセット型 (AbstractTestSet
の部分型) を指定でき、指定すると入れ子になった @testset
の呼び出しでも同じ CustomTestSet
が使われます。独自のテストセット型が使われるのはそれが指定されたテストセットでのみです。省略するとデフォルトのテストセット型が使われます。
テストセットを説明する文字列ではループ変数の補間が可能です。説明文字列を与えないと、ループ変数を使って自動的に構築されます。
デフォルトで @testset
マクロはテストセットを表すオブジェクトをそのまま返しますが、この振る舞いは独自のテストセット型を使ってカスタマイズできます。for
ループを与えると、@testset
マクロは各反復に対応するテストセットに対する finish
メソッドの返り値を収集してそれを返します。デフォルトの finish
は受け取ったテストセットオブジェクトをそのまま返すので、for
ループではテストセットオブジェクトの配列が返ります。
@testset
の本体を実行する前に Random.seed!(seed)
が自動的に呼ばれます。ここで seed
は現在のグローバル RNG の現在のシードです。さらに本体の実行が終わるとグローバル RNG の状態は @testset
の前の状態に復元されます。これはテストが失敗した場合にも容易に再現性が得られるようにするためであり、グローバル RNG の状態に対する副作用を持つ @testset
であっても自由な並べ替えが可能になります。
例
julia> @testset "trigonometric identities" begin
θ = 2/3*π
@test sin(-θ) ≈ -sin(θ)
@test cos(-θ) ≈ cos(θ)
@test sin(2θ) ≈ 2*sin(θ)*cos(θ)
@test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
end;
Test Summary: | Pass Total
trigonometric identities | 4 4
前に示した foo(x)
関数のテストをテストセットに入れると次のようになります:
julia> @testset "Foo Tests" begin
@test foo("a") == 1
@test foo("ab") == 4
@test foo("abc") == 9
end;
Test Summary: | Pass Total
Foo Tests | 3 3
テストセットは入れ子にできます:
julia> @testset "Foo Tests" begin
@testset "Animals" begin
@test foo("cat") == 9
@test foo("dog") == foo("cat")
end
@testset "Arrays $i" for i in 1:3
@test foo(zeros(i)) == i^2
@test foo(fill(1.0, i)) == i^2
end
end;
Test Summary: | Pass Total
Foo Tests | 8 8
この例のように入れ子になったテストセットで失敗が起こらないと、そのセットは要約に表示されません。失敗が起こると、そのテストセットに関する詳細だけが表示されます:
julia> @testset "Foo Tests" begin
@testset "Animals" begin
@testset "Felines" begin
@test foo("cat") == 9
end
@testset "Canines" begin
@test foo("dog") == 9
end
end
@testset "Arrays" begin
@test foo(zeros(2)) == 4
@test foo(fill(1.0, 4)) == 15
end
end
Arrays: Test Failed
Expression: foo(fill(1.0, 4)) == 15
Evaluated: 16 == 15
[...]
Test Summary: | Pass Fail Total
Foo Tests | 3 1 4
Animals | 2 2
Arrays | 1 1 2
ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.
その他のテストマクロ
浮動小数点数の計算は正確でないので、近似的な等価判定は @test a ≈ b
または直接 isapprox
を使って行います (≈
は \approx
をタブ補完することで入力できます)。
julia> @test 1 ≈ 0.999999999
Test Passed
julia> @test 1 ≈ 0.999999
Test Failed at none:1
Expression: 1 ≈ 0.999999
Evaluated: 1 ≈ 0.999999
ERROR: There was an error during testing
Test.@inferred
── マクロ
@inferred [AllowedType] f(x)
関数呼び出しの式 f(x)
がコンパイラが推論するのと同じ型の値を返すことをテストします。型安全性を確認するときに有用です。
f(x)
には任意の関数呼び出し式を指定できます。型が一致するなら f(x)
の評価結果を返し、一致しないなら Error
型の値を返します (Error
は Result
の部分型です)。
省略可能な引数 AllowedType
を渡すとテストの条件が緩和されます。具体的には、推論された型の AllowedType
を法としたモジュロと f(x)
の返り値の型が一致するとき1、および返り値の型が AllowedType
の部分型であるときにテストがパスするようになります。これは Union{Nothing, T}
や Union{Missing, T}
といった小さな型共用体を返す関数の型安定性をテストするときに有用です。
julia> f(a) = a > 1 ? 1 : 1.0
f (generic function with 1 method)
julia> typeof(f(2))
Int64
julia> @code_warntype f(2)
Variables
#self#::Core.Compiler.Const(f, false)
a::Int64
Body::UNION{FLOAT64, INT64}
1 ─ %1 = (a > 1)::Bool
└── goto #3 if not %1
2 ─ return 1
3 ─ return 1.0
julia> @inferred f(2)
ERROR: return type Int64 does not match inferred return type Union{Float64, Int64}
[...]
julia> @inferred max(1, 2)
2
julia> g(a) = a < 10 ? missing : 1.0
g (generic function with 1 method)
julia> @inferred g(20)
ERROR: return type Float64 does not match inferred return type Union{Missing, Float64}
[...]
julia> @inferred Missing g(20)
1.0
julia> h(a) = a < 10 ? missing : f(a)
h (generic function with 1 method)
julia> @inferred Missing h(20)
ERROR: return type Int64 does not match inferred return type Union{Missing, Float64, Int64}
[...]
Test.@test_logs
── マクロ
@test_logs [log_patterns...] [keywords] expression
expression
を実行し、生成されるログレコードを collect_test_logs
で収集し、それが log_patterns
とマッチすることを確認し、expression
の評価結果を返します。keywords
はログレコードの簡単なフィルタリングを提供します: キーワード引数 min_level
は収集/テストが行われる最小ログレベルを制御し、キーワード引数 match_mode
はログレコードと log_pattern
のマッチを判定する方法を定義します。match_mode
がデフォルトの :all
だとログレコードの各要素と log_pattern
の各要素に対して組ごとにマッチすることが確認され、:any
だとログレコードのどれか一つと log_pattern
がマッチすることが確認されます。
最も有用な log_patterns
は (level,message)
の形をした単純なタプルです。これよりも多い要素数のタプルを指定すると、handle_message
関数を通して AbstractLogger
に渡されたログメタデータの引数 (level,message,module,group,id,file,line)
と (タプルの長さだけ) マッチします。デフォルトではパターンを表すタプルに存在する要素とログフィールドは要素ごとに ==
で比較されます。ただし例外的に標準のログレベルとのマッチではパターンにシンボルが利用でき、文字列またはシンボルのフィールドと Regex
のパターンは occursin
で比較されます。
例
一つの @info
といくつかの @debug
をログとして発生させる関数を考えます:
function foo(n)
@info "Doing foo with n=$n"
for i=1:n
@debug "Iteration $i"
end
42
end
@info
によるログは次のコードでテストできます:
@test_logs (:info,"Doing foo with n=2") foo(2)
@debug
によるログもテストしたい場合には、キーワード引数 min_level
で @debug
を有効化する必要があります:
@test_logs((:info,"Doing foo with n=2"),
(:debug,"Iteration 1"),
(:debug,"Iteration 2"),
min_level=Debug,
foo(2))
特定のログだけをテストして他は無視したい場合には、キーワード引数 match_mode=:any
を使います:
@test_logs((:info,),
(:debug,"Iteration 42"),
min_level=Debug,
match_mode=:any,
foo(100))
@test_logs
と @test
を連鎖させて返り値をテストすることもできます:
@test (@test_logs (:info,"Doing foo with n=2") foo(2)) == 42
Test.@test_deprecated
── マクロ
@test_deprecated [pattern] expression
コマンドライン引数で --depwarn=yes
が指定されているなら、expression
が非推奨警告を発生させることをテストし、expression
の評価結果を返します。そのときログメッセージの文字列が pattern
にマッチすることが確認されます。pattern
のデフォルト値は r"deprecated"i
です。
--depwarn=no
のときは何もせずに expression
の評価結果を返します。--depwarn=error
なら ErrorExcaption
が発生することが確認されます。
例
# Julia 0.7 で非推奨
@test_deprecated num2hex(1)
# @test を使うと返り値をテストできる。
@test (@test_deprecated num2hex(1)) == "0000000000000001"
Test.@test_warn
── マクロ
@test_warn msg expr
expr
を評価したときの stderr
への出力が文字列もしくは正規表現 msg
とマッチすることをテストします。msg
が真偽値を返す関数なら、msg(output)
が true
を返すことがテストされます。msg
がタプルまたは配列なら、エラー出力が msg
の各要素とマッチすることがテストされます。expr
の評価結果を返します。
エラー出力が存在しないことを確認するには @test_nowarn
を使ってください。
注意: @warn
が生成する警告はこのマクロでテストできません。@test_logs
を使ってください。
Test.@test_nowarn
── マクロ
@test_nowarn expr
expr
を評価したときに stderr
に何も出力されないこと (警告などのメッセージがないこと) をテストします。expr
の評価結果を返します。
注意: @warn
が生成する警告が存在しないことはこのマクロでテストできません。@test_logs
を使ってください。
壊れたテスト
特定のテストが失敗していて手の施しようがないなら、そのテストでは @teest_broken
マクロを使うこともできます。このマクロはテストが失敗したときは Broken
を返して実行を続け、成功したときにだけ Error
でユーザーに報告します。
Test.@test_broken
── マクロ
@test_broken ex
@test_broken f(args...) key=val ...
「このテストはパスするべきだが、現在は確実に失敗する」という印をテストに付けます。式 ex
が false
に評価されることをテストし、true
に評価されたら例外を送出します。テストが失敗したら Broken
型の値を返し、成功したら Error
型の値を返します (Broken
と Error
は Result
の部分型です)。
@test_broken f(args...) key=val...
の意味は @test
と同様です。
例
julia> @test_broken 1 == 2
Test Broken
Expression: 1 == 2
julia> @test_broken 1 == 2 atol=0.1
Test Broken
Expression: ==(1, 2, atol = 0.1)
テストを評価せずに飛ばすマクロとして @test_skip
もあります。ただしテストが実行されなくても Broken
型の値は返るので、テストセットの要約には表示されます。
Test.@test_skip
── マクロ
@test_skip ex
@test_skip f(args...) key=val ...
「このテストを実行してはいけないが、テストセットの要約には Broken
として含まれるべきである」という印をテストに付けます。断続的に失敗するテストや実装されていない機能のテストでこのマクロは有用です。
@test_skip f(args...) key=val...
の意味は @test
と同様です。
例
julia> @test_skip 1 == 2
Test Broken
Skipped: 1 == 2
julia> @test_skip 1 == 2 atol=0.1
Test Broken
Skipped: ==(1, 2, atol = 0.1)
独自の AbstractTestSet
型の作成
パッケージは record
と finish
を実装することで AbstractTestSet
の部分型を独自に作成できます。新しい部分型はテストセットを説明する文字列を受け取る一引数のコンストラクタを持つべきです (他のオプションはキーワード引数で渡します)。
Test.record
── 関数
record(ts::AbstractTestSet, res::Result)
テストセットに結果を記録します。この関数はテストセットに含まれる @test
マクロが完了するたびに @testset
機構によって呼ばれ、そのときテストの結果が渡されます (結果は Error
である可能性もあります)。またテストブロック内の @test
コンテキストでない場所で例外が発生したときにも、record
は Error
と共に呼ばれます。
Test.finish
── 関数
finish(ts::AbstractTestSet)
テストセットで必要な最終処理を行います。これは @testset
機構によってテストブロックの実行が終わったときに呼ばれます。この関数では get_testset
を使ってテストセットを親の結果リストに追加する処理がよく行われます。
テストセットの実行中に入れ子になったテストセットのスタックを管理する責任は Test
モジュールにありますが、そのスタックにテスト結果を積むのは AbstractTestSet
の部分型の責任です。このスタックには get_testset
メソッドと get_testset_depth
メソッドでアクセスできます。この二つの関数はエクスポートされていないことに注意してください。
Test.get_testset
── 関数
get_testset()
アクティブなテストセットをタスクローカルのストレージから取得します。アクティブなテストセットが存在しなければ、デフォルトの FallbackTestSet
を返します。
Test.get_testset_depth
── 関数
入れ子になった @testset
の呼び出しに AbstractTestSet
の部分型が渡されないときは親と同じものが自動的に使われますが、この処理も Test
モジュールによって行われます。ただし親のテストセットが持つプロパティは伝播しません。パッケージでテストセットが持つプロパティの継承を実装したい場合には、Test
モジュールが提供するスタック機構を使って実装してください。
AbstractTestSet
の部分型の定義は次のような形になるでしょう:
import Test: Test, record, finish
using Test: AbstractTestSet, Result, Pass, Fail, Error
using Test: get_testset_depth, get_testset
struct CustomTestSet <: Test.AbstractTestSet
description::AbstractString
foo::Int
results::Vector
# コンストラクタはテストセットを説明する文字列と
# オプションを表すキーワード引数を受け取る。
CustomTestSet(desc; foo=1) = new(desc, foo, [])
end
record(ts::CustomTestSet, child::AbstractTestSet) = push!(ts.results, child)
record(ts::CustomTestSet, res::Result) = push!(ts.results, res)
function finish(ts::CustomTestSet)
# トップレベルでないなら、親のテストセットに自身を記録する。
if get_testset_depth() > 0
record(get_testset(), ts)
end
ts
end
テストセットは次のように使います:
@testset CustomTestSet foo=4 "custom testset inner 2" begin
# このテストセットは CustomTestSet 型を継承するが、引数は継承しない。
@testset "custom testset inner" begin
@test true
end
end