okhttp の MockWebServer で指定する Dispatcher でリクエストカウントを参照

oboe2uran.hatenablog.com

void okhttp3.mockwebserver.MockWebServer.enqueue(@NotNull MockResponse response)
は、キューとして応答レスポンスを登録するので返す順番にこれを呼べばよかった。

Dispatcher の方法で、リクエストカウントを判断したい
MockWebServer の getRequestCount() メソッドで、1始まりのカウントを取得できる。

特に高度なテクニックなコードではない。単純に生成する MockWebServer を Dispatcher 内で使うだけ。

MockWebServer server = new MockWebServer();
Dispatcher dispatcher = new Dispatcher() {
    @Override
    public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
        int requestCount = server.getRequestCount();
        // TODO リクエスト回数による応答をするようにする → editBody(requestCount)
        return new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
            .setBody( editBody(requestCount)  ).setResponseCode(200);
    }
};
server.setDispatcher(dispatcher);

JUnit JSON を検証したい

JUnit テストで生成されるJSON を期待値 JSONテキストと比較して検証したい。
hamcrest-json というのを使うとできる。

Hamcrest Related Projects
ここから、
GitHub - hertzsprung/hamcrest-json: Hamcrest matchers for comparing JSON documents
を辿ると見つかる。
現在、Maven セントラルリポジトリにあるバージョンは、0.2 であった。

<dependency>
   <groupId>uk.co.datumedge</groupId>
   <artifactId>hamcrest-json</artifactId>
   <version>0.2</version>
   <scope>test</scope
</dependency>

使用サンプル

@Test
public void test() {
    // TODO テスト実行
    try{
        // 期待値JSON読込み
        String expectJson = Files.readString(
        Path.of(Thread.currentThread().getContextClassLoader()
                         .getResource("excpect.json").toURI())
               , StandardCharsets.UTF_8);
        
        //String actualJson = テストで取得する結果 JSON テキスト
        
        // 検証
        MatcherAssert.assertThat(actualJson, SameJSONAs.sameJSONAs(expectJson));
   
    }catch(IOException e){
        e.printStackTrace();
        Assert.fail();
    }catch(URISyntaxException e){
        e.printStackTrace();
        Assert.fail();
    }
}

キーが多い時
expectJson より、actualJson で、追加キーが存在する場合は、、

MatcherAssert.assertThat(actualJson, SameJSONAs.sameJSONAs(expectJson).allowingExtraUnexpectedFields());

配列の順番を問わない
expectJson と、actualJson で、中の配列の値が変わらず順番を問わない場合は、、

MatcherAssert.assertThat(actualJson, SameJSONAs.sameJSONAs(expectJson).allowingAnyArrayOrdering());

キーが多い時 AND 配列の順番を問わない
allowingExtraUnexpectedFields() と、allowingAnyArrayOrdering() を連結で良い

MatcherAssert.assertThat(actualJson, SameJSONAs.sameJSONAs(expectJson).allowingExtraUnexpectedFields().allowingAnyArrayOrdering());

JUnit 実行で、ログファイルをテストケース毎に保存させる。

org.junit.rules.TestName から実行時のテストケースメソッド名を取得して
メソッド名のディレクトリにログ出力をテストケース単位にログファイルとして保存します。

SL4J + log4j2 を使用している場合と、SL4J + logback を使用している場合、
各々退避方法に工夫が必要です。

SL4J + log4j2 を使用している場合、

log4j2.xml に対してテストで使用する test-log4j2.xml が以下であるとします。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
   <Properties>
      <Property name="format1">[%d{yyyy-MM-dd HH:mm:ss.SSS}],[%-5p], %c, %m%n</Property>
   </Properties>
   <Appenders>
      <Console name="Console" target="SYSTEM_OUT">
         <PatternLayout>
            <pattern>${format1}</pattern>
         </PatternLayout>
      </Console>
      <!-- ########## test-log4j2.xml だけに記載する ############ -->
      <File name="file" fileName="/work/console.log">
         <PatternLayout>
            <pattern>${format1}</pattern>
         </PatternLayout>
      </File>
      <!-- ##################################################### -->
   </Appenders>
   <Loggers>
      <Root level="trace">
         <AppenderRef ref="Console" />
         <!-- ########## test-log4j2.xml だけに記載する ############ -->
         <AppenderRef ref="file" />
         <!-- ##################################################### -->
      </Root>

      <Logger name="org.apache.ibatis.transaction" level="WARN" />
      <Logger name="org.mybatis.guice.transactional" level="WARN" />

      <Logger name="org.myproject" level="INFO" />
   </Loggers>
</Configuration>

JUnit のソース
@BeforeClass メソッド=このJUnitインスタンス生成時だけ動くメソッドで
test-log4j2.xml で書いた "file" の FileAppender を探し出して、
コンソール標準出力したログファイルパスを記憶させます。
@After メソッドで
LogManager.shutdown(); による一時的なログ出力の中断を発生させて、、
記憶させたログファイルパスから、org.junit.rules.TestName で取得する
テストケースメソッド名に沿って、ログファイルを移動します。

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.FileAppender;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class SampleTest{
    private static String logfilepath;
    @Rule  public TestName testName = new TestName();

    @BeforeClass
    public static void initialize(){
        System.setProperty("log4j.configurationFile", "test-log4j2.xml");
        try(LoggerContext ctx = (LoggerContext) LogManager.getContext()){
             logfilepath = ((FileAppender)ctx.getConfiguration().getAppender("file")).getFileName();
        }
    }

    @Before
    public void before() {
        // TODO
    }
    @After
    public void after() {
        // LogManager shutdown
        LogManager.shutdown();
        // 退避先を生成
        File testOutDir = new File("/logrecord/" + testName.getMethodName());
        testOutDir.mkdirs();
        // ログファイルを移動
        try{
            Files.move(Paths.get(logfilepath), Paths.get(testOutDir.getAbsolutePath() + "/console.log"), StandardCopyOption.REPLACE_EXISTING);
        }catch(IOException e){
            e.printStackTrace();
            Assert.fail();
        }
    }
    @Test
    public void test1() {
        // TODO
    }
    @Test
    public void test2() {
        // TODO
    }
}

これで、/logrecord/test1/console.log と /logrecord/test2/console.log が保存されます。

SL4J + logback を使用している場合、

logback.xml に対してテストで使用する test-logback.xml が以下であるとします。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>
   <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <Target>System.out</Target>
      <encoder>
         <Pattern>%-23d{yyyy/MM/dd HH:mm:ss.SSS} %-5p [%thread] %m\t\t\t[%C{0}.%method:%line]%n</Pattern>
      </encoder>
   </appender>
   <!-- ########## test-logback.xml だけに記載する ###################################### -->
   <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <File>/work/console.log</File>
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <FileNamePattern>/var/log/labo.%d{yyyy-MM-dd}.log</FileNamePattern>
         <maxHistory>6</maxHistory>
      </rollingPolicy>
      <encoder>
         <charset>UTF-8</charset>
         <Pattern>%-23d{yyyy/MM/dd HH:mm:ss.SSS} %-5p [%thread] %m\t\t\t[%C{0}.%method]%n</Pattern>
      </encoder>
   </appender>
   <!-- ################################################################################# -->

   <logger name="org.myproject">
      <level value="debug" />
      <appender-ref ref="STDOUT" />
      <!-- ########## test-logback.xml だけに記載する ###################################### -->
      <appender-ref ref="FILE" />
      <!-- ################################################################################# -->
   </logger>

</configuration>

log4j2 のような、LogManager shutdown がありません。
テストケースの @After で、ファイルコピー後に、空文字でログファイルを上書きするという
ちょっとセンスがない方法しかないようです。
FileAppender を探索する方法もちょっと面倒な方法をしなければなりません。

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.FileAppender;
@RunWith(JUnit4.class)
public class SampleTest{
    @Rule  public TestName testName = new TestName();
    private static File appenderFile;

    @BeforeClass
    public static void initialize(){
        System.setProperty("logback.configurationFile","test-logback.xml");
        // FileAppenderを探索して、ログ Fileを求める
        appenderFile = getAppenderFile("FILE");
    }

    @Before
    public void before() {
        // TODO
    }
    @After
    public void after() {
        // 退避先を生成
        File testOutDir = new File("/logrecord/" + testName.getMethodName());
        testOutDir.mkdirs();
        // コピーで退避
        try{
            Files.copy(Paths.get(appenderFile.getAbsolutePath()), Paths.get(testOutDir.getAbsolutePath() + "/console.log"), StandardCopyOption.REPLACE_EXISTING);
        }catch(IOException e){
            e.printStackTrace();
            Assert.fail();
        }
        // ログファイルを空文字で上書きする
        try(FileWriter writer = new FileWriter(appenderFile)){
            writer.write("");
            writer.flush();
        }catch(IOException e){
            e.printStackTrace();
            Assert.fail();
        }
    }
    private static File getAppenderFile(String appenderName) {
        ch.qos.logback.classic.LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
        for(ch.qos.logback.classic.Logger logger : lc.getLoggerList()){
            Iterator<Appender<ILoggingEvent>> appenderIterator = logger.iteratorForAppenders();
            while(appenderIterator.hasNext()){
                Appender<ILoggingEvent> appender = appenderIterator.next();
                if (appender instanceof FileAppender){
                    FileAppender<ILoggingEvent> fileappender = (FileAppender<ILoggingEvent>)appender;
                    if (appenderName.equals(fileappender.getName())) {
                        return new File(fileappender.getFile());
                    }
                }
            }
        }
        throw new IllegalArgumentException("FileAppender not found name : "+appenderName);
    }
    @Test
    public void test1() {
        // TODO
    }
    @Test
    public void test2() {
        // TODO
    }
}

JSON シリアライズ時のソート

Java Jackson を使用した時のシリアライズのソートの話である。
@JsonPropertyOrder を使わないで、ObjectMapper の設定として指定する方法

Java Object プロパティ順(フィールド宣言順)
Jackson version >= 2.6.1

ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);

アルファベット順

// @deprecated Since 2.13 use {@code JsonMapper.builder().configure(...)}
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);

バージョン 2.13 以降では、MapperFeature そのものが @Deprecated なのではなく
configure メソッドで、MapperFeature を指定することが @Deprecated になっている。

代わりに、アルファベット順を指定できる方法は、SerializationFeature では
アルファベット順指定が存在しないので、SerializationConfig を取得して
MapperFeature.SORT_PROPERTIES_ALPHABETICALLY でセットして再セットする。

mapper.setConfig(
   mapper.getSerializationConfig()
   .with(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
);


Java Object プロパティ順(フィールド宣言順)も同じよう設定するなら
MapperFeature.SORT_CREATOR_PROPERTIES_FIRST でセットして再セットする。

mapper.setConfig(
   mapper.getSerializationConfig()
   .with(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST)
);


整形してシリアライズ

DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter("    ", DefaultIndenter.SYS_LF);
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
printer.indentObjectsWith(indenter);
printer.indentArraysWith(indenter);

String res = mapper.writer(printer).writeValueAsString(object);

Jackson シリアライズで、null オブジェクトを出力させない

一律に全て、null オブジェクトを出力させない

import com.fasterxml.jackson.annotation.JsonInclude.Include;

Include.NON_NULL を設定する。

ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);

Jackson JSONシリアライズ実行で、null を空文字にする

汎用的にシリアライザを設定するのが賢い
com.fasterxml.jackson.databind.JsonSerializer 実装を
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider
で、約束して ObjectMapper に登録する。

DefaultSerializerProvider が抱える
public final static class Impl extends DefaultSerializerProvider

Method that can be used to specify serializer that will be
used to write JSON values matching Java null values
instead of default one (which simply writes JSON null).

と、NULL の時の設定メソッド setNullValueSerializer で設定する。

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
DefaultSerializerProvider dsp = new DefaultSerializerProvider.Impl();
dsp.setNullValueSerializer(new JsonSerializer<Object>(){
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException{
        gen.writeString("");
    }
});
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerProvider(dsp);


デフォルトの JSON整形

DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
printer.indentObjectsWith(new DefaultIndenter());
printer.indentArraysWith(new DefaultIndenter());
ObjectMapper mapper = new ObjectMapper();
String res = mapper.writer(printer).writeValueAsString(some);

JUnit 用のログ設定を行う

リリース用のログ設定ファイル、
  logback 使用の場合 → logback.xml
  log4j2 使用の場合 → log4j2.xml
これを src/main/resources に配置するが、Junitテスト用のログ設定を別に用意したい時、、、

import org.junit.BeforeClass;

システムプロパティで、設定ファイルの読込み先指定を
@BeforeClass 付与の static メソッドで、実行する。

logback テスト用 test-logback.xml を使用

@BeforeClass
public static void initialize(){
    System.setProperty("logback.configurationFile","test-logback.xml");
}

src/test/resources に test-logback.xml を配置する。


log4j2 テスト用 test-log4j2.xml を使用

@BeforeClass
public static void initialize(){
    System.setProperty("log4j.configurationFile","test-log4j2.xml");
}

src/test/resources に test-log4j2.xml を配置する。