りんごがでている

何か役に立つことを書きます

Juliaのユニットテスト

Julia Advent Calendar 9日目の記事です。なんとか途切れさせないでいきましょう。

Juliaと言えばユニットテストが書きやすい言語として有名です[要出典]。 何故書きやすいのかといえば、もちろんマクロがあるからです。 マクロのおかげで、よく分からないアサーションのコードをたくさん覚えなくて済みます。 この記事では、そのへんの理由や、Juliaでの最近のユニットテストの書き方について説明しようと思います。

Juliaのユニットテストの良いところ

以下のテストコードはsumという関数の動作をテストしている想定です:

julia> using Base.Test

# ok
julia> @test sum([1,2,3]) == 6

# not ok
julia> @test sum([1,2,3]) == 7
ERROR: test failed: 6 == 7
 in expression: sum([1,2,3]) == 7
 in error at /usr/local/julia/v0.4/lib/julia/sys.dylib
 in default_handler at test.jl:30
 in do_test at test.jl:53

# ok
julia> @test isa(sum([1,2,3]), Int)

# not ok
julia> @test isa(sum([1,2,3]), Float64)
ERROR: test failed: isa(sum([1,2,3]),Float64)
 in expression: isa(sum([1,2,3]),Float64)
 in error at /usr/local/julia/v0.4/lib/julia/sys.dylib
 in default_handler at test.jl:30
 in do_test at test.jl:53

ここで使われているのは@testというマクロです。 テストが失敗している方に注目していただきたのですが、エラーメッセージにtest failed: 6 == 7test failed: isa(sum([1,2,3]),Float64)のように、どの条件が失敗したのかが分かりやすく現れています。これは、@testマクロがテストの条件式をとり、その式自体を表示できるためです。特に、==などの二項演算子の場合は、左辺の値 sum([1,2,3]) の結果もエラーメッセージに表示してくれていることに気をつけてください。

マクロのない言語だと、テストするだけでもアサーションの関数が山盛りになってしまいます。 例えば、Pythonunittestだと、assertEqual(first, second, msg=None)assertIsInstance(obj, cls, msg=None)のような、ある条件を判定するアサーション関数がたくさん用意されています。ちょっとコレを全部覚えるのは辛いですし、全部assertTrue(expr, msg=None)を使うとテストがどう失敗したのかが分かりづらくなってしまいます。

モダンなユニットテストの方法

とは言え、@testマクロだけではちょっと機能が不足している面もあるでしょう。現在のリリース版v0.4系に付属しているBase.Testには、関係のあるテストをまとめる機能がありません。そのため、テストがフラットで把握しにくい構造になってしまいます。

次期バージョンのv0.5ではBase.Testが一新され、より構造化しやすくなっています。v0.5では@testsetというマクロが新たに追加され、テストをまとめることができるようになりました。 これは、@testset ["description"] begin ... endのように使い、テストの説明と複数のテストをまとめるブロックを取ることができるようになっています。さらに、テストセットの結果はまとめて報告されます:

julia> using Base.Test

julia> @testset "sum" begin
           @test sum([1,2,3]) == 6
           @test isa(sum([1,2,3]), Int)
           @test sum(1:3) == 6
       end
Test Summary: | Pass  Total
sum           |    3      3
Base.Test.DefaultTestSet("sum",Any[Test Passed
  Expression: sum([1,2,3]) == 6
   Evaluated: 6 == 6,Test Passed
  Expression: isa(sum([1,2,3]),Int),Test Passed
  Expression: sum(1:3) == 6
   Evaluated: 6 == 6],false)

実は、テスト結果のサマリーには色もつくようになりました。

f:id:bicycle1885:20151208184155p:plain

さらに、@testsetは以下のように何段でもネストできます:

@testset "basic functions" begin
    @testset "sum" begin
        @test ...
        @test ...
    end

    @testset "mean" begin
        @test ...
        @test ...
    end

    ...
end

@testsetがブロックとして取れるのはbegin ... endだけでなく、for ... endも取ることができます。 したがって、パラメータaを変えながらテストをしたい場合などは@testset for a in 1:10 ... endとすればよいでしょう。 実はこの機能はちょっと前まで@testloopとして提供されていたのですが、@testsetと統合してはどうかと提案したらすんなり受け入れられて現在はそのようになっています。Juliaってオープンですね(´ε` )

他にも、テストのレポートの仕方を変えるなど様々な拡張が可能になりました。 詳しい仕様は、v0.5のドキュメントを参照してください。

v0.5の便利なユニットテストを今使う

v0.5からは標準で@testsetが使えるようになるわけですが、やっぱり今v0.4で使いたいですね。 そのためには、BaseTestNext.jlを使いましょう。 以下のように書けば、v0.4向けのパッケージでも次世代のBase.Testを使うことができます。

if VERSION >= v"0.5-"
    using Base.Test
else
    using BaseTestNext
    const Test = BaseTestNext
end