1時に寝て5時に起きて2度寝して9時に起きた。前日呑んでたのであまり眠れなくて体調よくない。

JUnit5 的なロガーのテスト

お仕事でログ管理の機能開発をしている。カスタムロガーを使って出力するメッセージを加工している。設計が固まってきて機能も作り込むようになってきたので出力内容が意図した構造化ログになっているかをテストしたい。JUnit5 の機能と log4j の機能を組み合わせてカスタムロガーのテストの仕組みを作ってみた。

まずログ出力した内容を取得するオブジェクトを特定するためのアノテーションを定義する。

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggerTestWriter {
}

JUnit5 の Declarative Extension Registration の仕組みを使って、テストケース非依存な setup/teadown のメソッドを定義する。ExtensionContext から拡張するテストケースのインスタンスを取得できる。テストケースインスタンスに定義されている @LoggerTestWriter アノテーションがついたオブジェクトを lgo4j の Appender としてインジェクションするようなコードを setup/teardown (beforeEach/afterEach メソッド) で定義する。Appender のインジェクション周りは Log4j 2でログ出力をテストするサンプルソース の記事を参考にした。

public class SetupLogAppender implements BeforeEachCallback, AfterEachCallback {
    private static String APPENDER_NAME = "logger-test-appender";

    private Optional<Writer> getWriter(ExtensionContext context) throws IllegalAccessException {
        var testInstance = context.getRequiredTestInstance();
        for (var field : testInstance.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(LoggerTestWriter.class)) {
                return Optional.of((Writer) field.get(testInstance));
            }
        }
        return Optional.empty();
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        var writer = this.getWriter(context).orElseThrow(() ->
                new IllegalStateException("@LoggerTestWriter のアノテーションをもつ Writer を定義してください"));
        addAppender(writer, APPENDER_NAME);
    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        var writer = this.getWriter(context).orElseThrow(IllegalStateException::new);
        removeAppender(APPENDER_NAME);
        if (writer instanceof StringWriter) {
            var stringWriter = (StringWriter) writer;
            stringWriter.getBuffer().delete(0, stringWriter.getBuffer().length());
        }
    }

実際にテストを書くテストクラスは次のようになる。

@ExtendWith(SetupLogAppender.class)
public class MyLoggerTest {
    private static final MyLogger logger = new MyLogger(MyLoggerTest.class.getName());

    @LoggerTestWriter
    StringWriter writer = new StringWriter();

    @Test
    void testDebugMap() {
        logger.debug("my-message")
        assertEquals("my-message" writer.toString());
    }

@ExtendWith で指定した SetupLogAppender クラスの beforeEach や afterEach がそれぞれのテストメソッドごとに呼ばれて、Appender のインジェクションが @LoggerTestWriter のアノテーションをもつ writer を使って行われる。この writer にはログ出力した文字列が記録されるようになる。これで、テストメソッドで logger に対して出力したメッセージを writer から取得できるので意図したメッセージが出力されていることをテストできる。カスタムロガーのテストケースごとに再利用可能な拡張をきれいに実装できた。