💔

`@test_broken ..`よりも`@test .. broken=..`の方が良かった

2023/12/05に公開

これはJulia Advent Calendar 2023の5日目の記事です。

TL;DR

@test_broken hogehoge

よりも

@test hogehoge broken=true

の方が幸せになれるかも知れない。

Juliaのテストの書き方の復習

Juliaでは以下のようにテストが書けます。

  • @test exextrueであることをテストする
  • @testsetでグループ分け
@testset "arithmetic operations" begin
    @test 1 + 1 == 2
    @test 3 / 2 == 1.5
    @test 3 ÷ 2 == 1
    @test isnan(0/0)
end
  • エラーをテストしたい場合は@test_throwsを使う
  • @test_throwsの最初の引数はエラーの型。
@testset "sqrt operations" begin
    @test_throws DomainError sqrt(-1)  # 複素数が欲しければ `sqrt(-1+0im)`と書けば良い
end
  • 実装不備によってテストが失敗することをテストしたい場合は@test_brokenを使う
  • @test_brokenが成功(true)ならエラーとして扱われる
my_sinc(x) = sin(x)/x
@test_broken my_sinc(0) == 1

以上の例を合わせてREPLで実行すると以下のような出力になります。

テスト結果の要約と実行時間の出力が見やすいですね。

ケース①

問題

Base.oneを自分で実装したmy_one関数を作ってテストしてみましょう。
ただし、わざと間違えた定義で。

my_one(::Type{Float64}) = 1.0
my_one(::Type{Float32}) = 1.0  # `1f0`が正しい定義
@testset "sometimes broken" begin
    @testset for T in (Float64, Float32)  # `@testset`は`for`にも使える!
        @test my_one(T) == 1
        @test_broken my_one(T) isa T  # `T`が`Float32`のときだけbroken
    end
end

@test_brokenT==Float64のときにbrokenじゃないので、エラーになってしまいましたね。

状況整理:

  • for文の中の@test_brokenなので、truefalse両方のケースを扱う必要がある
  • つまり、T===Float32のときだけ@test_brokenで、T===Float64のときは通常の@testを使いたい

解決策

そこで@test hogehoge broken=..ですよ!
これによってbrokentrueのときは@test@test_brokenとして扱われるようになります。

my_one(::Type{Float64}) = 1.0
my_one(::Type{Float32}) = 1.0
@testset "sometimes broken" begin
    @testset for T in (Float64, Float32)
        @test my_one(T) == 1
        @test my_one(T) isa T broken=(T===Float32)  # `T`が`Float32`のときだけbroken
    end
end

エラーが発生しませんでした。
brokenとして扱うケースをtrue/falseで指定できるので便利ですね。

ケース②

問題

自分で実装したmy_sin関数の数値精度を確認するテストを書いてみましょう。

my_sin(x) = x-x^3/6+x^5/120  # テイラー展開の打ち切り
@testset "sometimes broken" begin
    for x in range(-1,1,length=100)
        @test_broken abs(my_sin(x) - sin(x)) < 0.0001  # 10/100 のケースでbroken
    end
end


(長いエラー中略)

原点から離れるほど精度が落ちるので、その影響で左右5点ずつテストに通らなかったようですね。

解決策

ここでも@test hogehoge broken=..ですよ!

my_sin(x) = x-x^3/6+x^5/120  # テイラー展開の打ち切り
@testset "sometimes broken" begin
    for x in range(-1,1,length=100)
        # テストに失敗するケースを明示できないので`broken`にテスト内容を直接記載すればOK
        @test abs(my_sin(x) - sin(x)) < 0.0001 broken=!(abs(my_sin(x) - sin(x)) < 0.0001)
    end
end

エラーが消えましたね。やったぜ
my_sinを修正してbrokenじゃなくなった場合にbroken=..を消し忘れる可能性もありますが、エラーになるよりはマシでしょう。

まとめ & 補足

  • @testマクロはbrokenキーワード引数が便利。
    • forの中で部分的にbrokenになる場合に特に便利。
    • このキーワード引数は Julia v1.7 以上でしか使えないことに注意。
  • 公式ドキュメントで十分じゃなかったんですか?
    • 公式ドキュメントには@test 2 + 2 ≈ 5 atol=1 broken=false@test 2 + 2 ≈ 6 atol=1 broken=trueみたいな自明な例しか無く、brokenキーワード引数の本当の有り難みが伝わりにくい気がしました。
    • なのでこの記事が存在します。
GitHubで編集を提案

Discussion