Log4j 2でログ出力をテストするサンプルソース

Log4j 2でログ出力をテストするサンプルソースです。ちなみにLog4j 1.*系とはAPIが構造もろとも変わっており、色々書き直す必要があります。(`Logger`から`addAppender()`メソッドが無くなっています。)

結論、ここに本家ドキュメントの説明があるのですが、勉強の意味もこめて僕なりにサンプルを書いてみました。

http://logging.apache.org/log4j/2.x/manual/customconfig.html#AppendingToWritersAndOutputStreams

# ソース

## テスト対象のコード

``` SampleClass.java
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class SampleClass {
  private static final Logger LOGGER = LogManager.getLogger(SampleClass.class);

  void doSomething(String message) {
    // Java8 & Log4j2として、ラムダを使うことでinfoか否かの判定をLog4j2に実施させる。
    // みんなラムダで幸せになろうよ。
    LOGGER.info(() -> "message ->" + message);
  }
}
```
## テストコード

``` SampleClassTest.java
package org.example;

import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.WriterAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.junit.Test;

import java.io.StringWriter;
import java.io.Writer;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

public class SampleClassTest {
  static final String TEST_APPENDER_NAME = "fortest";

  @Test
  public void testDoSomething() throws Exception {
    StringWriter writer = new StringWriter();
    //現在のロガーに、writerを持つappenderを追加する。
    addAppender(writer, TEST_APPENDER_NAME);

    // テスト実行して、writerに流れてきている文字列を検証する。
    SampleClass sut = new SampleClass();
    sut.doSomething("nantoka");
    assertThat(writer.toString(), is("message ->nantoka" + System.lineSeparator()));

    // 追加したappenderを削除する。writerの中身も消す。
    removeAppender(TEST_APPENDER_NAME);
    writer.getBuffer().delete(0, writer.getBuffer().length());

    // もう一度テスト実行して、writerに流れてきている文字列を検証する。今度は空である。
    sut = new SampleClass();
    sut.doSomething("kantoka");
    assertTrue(writer.toString().length() == 0);
  }

  void addAppender(Writer writer, String name) {
    final LoggerContext context = LoggerContext.getContext(false);
    final Configuration config = context.getConfiguration();
    final PatternLayout layout = PatternLayout.createDefaultLayout(config);

    Appender appender = WriterAppender.createAppender(layout, null, writer, name, false, true);
    appender.start();
    config.addAppender(appender);
    updateLoggers(appender, config);
  }

  private void updateLoggers(final Appender appender, final Configuration config) {
    for (final LoggerConfig loggerConfig : config.getLoggers().values()) {
      loggerConfig.addAppender(appender, null, null);
    }
    config.getRootLogger().addAppender(appender, null, null);
  }

  private void removeAppender(String name) {
    final LoggerContext context = LoggerContext.getContext(false);
    final Configuration config = context.getConfiguration();
    for (final LoggerConfig loggerConfig : config.getLoggers().values()) {
      loggerConfig.removeAppender(name);
    }
    config.getRootLogger().removeAppender(name);
  }
}
```

## Log4j 2 設定ファイル

``` log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
  <appenders>
    <Console name="console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d %-5p [%t] - %m (%C.java:%L)%n"/>
    </Console>
  </appenders>

  <loggers>
    <Logger name="org.example" level="debug" additivity="false">
      <appender-ref level="info" ref="console"/>
    </Logger>
    <root level="trace">
      <appender-ref ref="console"/>
    </root>
  </loggers>
</configuration>
```

## gradle ビルドファイル

``` build.gradle
apply plugin: 'java'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

def defaultEncoding = 'UTF-8'

tasks.withType(AbstractCompile) each { it.options.encoding = defaultEncoding }

test {
  testLogging.showStandardStreams = true
}

repositories {
  jcenter()
}

dependencies {
  testCompile 'junit:junit:4.12'
  compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.5'
  compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.5'
}
```

# 実行結果

```
C:\home\idea\log4j2>gradle test
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

org.example.SampleClassTest > testDoSomething STANDARD_OUT
    2016-02-22 01:40:58,092 INFO  [Test worker] - message ->nantoka (org.example.SampleClass.java:11)
    2016-02-22 01:40:58,097 INFO  [Test worker] - message ->kantoka (org.example.SampleClass.java:11)

BUILD SUCCESSFUL

Total time: 14.312 secs
C:\home\idea\log4j2>
```

# 所感

Log4j2のドキュメントでは「プログラムでログ設定をいじるAPIを単純にしました。」とありますが、関わるクラスが 1.*  のときより増えているような気がしないでもありません。`Appender`の識別を名前ベースでできるようになったのは良いことだと思います。（ 1.* では、`Appender`のインスタンスそのもので識別でした。）私の習熟度もまだ低いので、writerを追加するためだけだったらもっと簡単に書けるかもしれません。

実際の使い方としては、`addAppender()`を`@Before`なメソッドに、`removeAppender()`を`@After`なメソッドに入れる事になると思います。`removeAppender()`しないと他のテストに影響を与えるかもしれません。不要な`Appender`が残り続けてメモリを圧迫するなどです。

ちなみに、Loggerをモックする方向も考えましたがライブラリの依存をなるべく減らしたいことと、Log4j2の機能で実現できるのでこちらを採用することにしました。

また、このやり方は現在の最新であるLog4j 2.5からの機能のようです。(リリースは2015-12-06) stackoverflowに同様の議論がありますが、古いバージョン向けのいかにもworkaroundのようなやり方なので、どちらかと言えばこのバージョンのこのやり方のほうが良いと思います。（それでもまだ簡素な感じはしませんが。）

# バージョン番号など

- Java8
- Log4j 2.5
- Gradle 2.11