S-JIS[2022-08-19] 変更履歴

JUnit5 Timeout

JUnit5のTimeoutアノテーションについて。


概要

JUnit5.5(正式版としてはJUnit5.7)でTimeoutアノテーションが追加された。

テストの実行が、Timeoutで指定した時間を経過するとタイムアウトする。
タイムアウトすると、そのテストは失敗扱いとなる。

import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

class TimeoutExample {

    @Test
    @Timeout(10) // 10秒でタイムアウト
    void sleep() throws InterruptedException {
        TimeUnit.SECONDS.sleep(30);
    }
}

このタイムアウトは、スレッドに割り込む(interruptする)ことで実現しているようだ。

このため、タイムアウト時間が経過したら即座にテストが停止するわけではない。
(テスト対象メソッドが割り込みを受けてから終了するまでの処理内容に依存する)

テスト対象がスレッドの割り込み(interrupt)を正しく処理していないと、タイムアウト(停止)しない。

タイムアウトの実現方法


タイムアウト時間の指定方法

 
秒を指定 @Timeout(10) // 10秒でタイムアウト
単位を指定 @Timeout(value = 1, unit = TimeUnit.MINUTES) // 1分でタイムアウト
クラスに指定 @Timeout(10) // 全テストメソッドにタイムアウトを適用
class TimtoutExample {
}

タイムアウトの実現方法

Timeoutアノテーションによるタイムアウトは、スレッドに割り込む(interruptする)ことで実現しているようだ。

Thread.sleep()の例

Thread.sleep()(TimeUnit.XXX.sleep())を使ってスリープしている場合、テストのタイムアウト時にsleepメソッドからInterruptedExceptionがスローされる。

    @Test
    @Timeout(10) // 10秒でタイムアウト
    void sleep() throws InterruptedException {
        TimeUnit.SECONDS.sleep(30); // 10秒経つとThreadがinterruptされてInterruptedExceptionがスローされる
    }

無限ループの例

以下のような無限ループだと、タイムアウト時間を経過してもテストは終了しない(タイムアウトしない)。

    @Test
    @Timeout(10) // 10秒でタイムアウト
    void loop() {
        for (;;) { // 無限ループ
        }
    }
    @Test
    @Timeout(10) // 10秒でタイムアウト
    void sleepLoop() {
        for (;;) { // 無限ループ
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException ignore) {
                // InterruptedExceptionを握りつぶす 
            }
        }
    }

※このような事があるので、Thread.sleep()のInterruptedExceptionは握りつぶしてはいけない。


割り込みを確認する例

Thread.interrupted()を呼び出せば、割り込みが発生しているかどうかを確認できる。

    @Test
    @Timeout(10) // 10秒でタイムアウト
    void loop() {
        for (;;) {
            if (Thread.interrupted()) {
                System.out.println("loop: interrupted");
                break; // ループを終了
            }
        }
    }

ちなみに、割り込みによってThread.interrupted()がtrueになるのは1回だけのようだ。
trueになってから次にThread.interrupted()を呼び出したら、falseが返る。


ソケット通信の例

java.net.SocketやServerSocketは、スレッドの割り込みに対応していない。
つまり、これらを使って通信しているメソッドをテストする場合、Timeoutアノテーションは機能しない。

SocketやServerSocketを使う場合は、そもそもソケットのタイムアウト時間を指定しておくべき。

    try (var socket = new Socket("localhost", 123)) {
        socket.setSoTimeout((int) TimeUnit.MINUTES.toMillis(1));
        〜
    }
    try (var server = new ServerSocket(123)) {
        server.setSoTimeout((int) TimeUnit.MINUTES.toMillis(30)); // acceptのタイムアウト時間

        try (var socket = server.accept()) {
            socket.setSoTimeout((int) TimeUnit.MINUTES.toMillis(1));
            〜
        }
    }

出力されるスタックトレース

タイムアウトしたときにテストメソッドから例外を投げると、タイムアウトの例外にaddSuppressedされる。

@Test
@Timeout(10)
void testSleep() throws InterruptedException {

    TimeUnit.SECONDS.sleep(30); //InterruptedException



}
@Test
@Timeout(10)
void testSleep() {
    try {
        TimeUnit.SECONDS.sleep(30);
    } catch (InterruptedException ignore) {
        return;
    }
}
java.util.concurrent.TimeoutException: testSleep() timed out after 10 second
at 〜
Suppressed: java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:337)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at com.example.junit5.TimeoutExample.sleep(TimeoutExample.java:14)
java.util.concurrent.TimeoutException: testSleep() timed out after 10 second
at 〜

JUnit5へ戻る / JUnitへ戻る / Javaへ戻る / 技術メモへ戻る
メールの送信先:ひしだま