CLOVER🍀

That was when it all began.

Spring Bootのテストで標準出力をキャプチャーする(OutputCapture/OutputCaptureExtension)

これは、なにをしたくて書いたもの?

Spring Bootを使ったアプリケーションのテストで、ログ出力した内容を確認するには?ということで。

調べてみるとOutputCaptureというものがあったので、こちらを使ってみます。

OutputCapture

OutputCaptureは、JUnitのExtensionとして提供されるSpring Bootのテストユーティリティで、標準出力・標準エラー出力をキャプチャー
できます。

Core Features / Testing / Test Utilities / OutputCapture

パッケージはorg.springframework.boot.test.systemです。

org.springframework.boot.test.system (Spring Boot 3.1.4 API)

使い方は、テストクラスに@ExtendWith(OutputCaptureExtension.class)を指定して

@ExtendWith(OutputCaptureExtension.class)
class MyTest {

OutputCaptureExtension (Spring Boot 3.1.4 API)

テストメソッドやコンストラクターCapturedOutputを受け取るようにします。

    @Test
    void test(CapturedOutput output) {
        System.out.println("ok");
        assertThat(output).contains("ok");
        System.err.println("error");
    }

CapturedOutput (Spring Boot 3.1.4 API)

使い方自体は簡単ですね。これらは、Spring Boot 2.2.0から使えるようです。

SLF4J+Logbackを使ってログを出力している時に、その内容を確認するにはどういうのがいいのかな?と思って調べてみたら
Spring Boot側で見つけることになりましたが。

当然ながら、ロギングライブラリーを使っている場合はログが標準出力に書き出されるように設定する必要があります。

また、java.util.loggingの場合は設定のリセットが必要だとOutputCaptureExtensionJavadocに書かれています。

   @AfterEach
    void reset() throws Exception {
        LogManager.getLogManager().readConfiguration();
    }

OutputCaptureExtension (Spring Boot 3.1.4 API)

それでは、試してみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.8.1 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.8.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-86-generic", arch: "amd64", family: "unix"

Spring Bootプロジェクトを作成する

それでは、Spring Bootプロジェクトを作成します。今回は特に依存関係は指定しません。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.1.4 \
  -d javaVersion=17 \
  -d type=maven-project \
  -d name=output-capture-example \
  -d groupId=org.littlewings \
  -d artifactId=output-capture-example \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.test \
  -d baseDir=output-capture-example | tar zxvf -

プロジェクト内に移動。

$ cd output-capture-example

Maven依存関係など。

        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

依存関係にspring-boot-starter-testがあれば今回はOKです。

自動生成されたソースコードは削除しておきます。

$ rm src/main/java/org/littlewings/spring/test/OutputCaptureExampleApplication.java src/test/java/org/littlewings/spring/test/OutputCaptureExampleApplicationTests.java

OutputCaptureを使ってみる

それでは、OutputCaptureを使ってみましょう。

ひとまずアプリケーションのエントリーポイントだけは作っておきます。

src/main/java/org/littlewings/spring/test/App.java

package org.littlewings.spring.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

ドキュメントに習って、簡単なものから。

src/test/java/org/littlewings/spring/test/SimpleOutputCaptureTest.java

package org.littlewings.spring.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
class SimpleOutputCaptureTest {
    @Test
    void captureOutput(CapturedOutput output) {
        System.out.println("Hello World");

        assertThat(output).isEqualTo("Hello World\n");  // CapturedOutput#getAllと同じ
        assertThat(output.getAll()).isEqualTo("Hello World\n");
        assertThat(output.getOut()).isEqualTo("Hello World\n");
        assertThat(output.getErr()).isEmpty();
    }

    @Test
    void contains(CapturedOutput output) {
        System.out.println("Hello World");
        System.out.println("Hello Spring");

        assertThat(output).contains("Hello World");
        assertThat(output).contains("Hello Spring");
    }
}

まずは@ExtendWith(OutputCaptureExtension.class)を付与します。@SpringTestなどがなくても使えます。

@ExtendWith(OutputCaptureExtension.class)
class SimpleOutputCaptureTest {

CapturedOutputをテストメソッドの引数(またはコンストラクタ)で受け取って、標準出力および標準エラー出力に書き出された後に
CapturedOutputに対してアサーションします。

    @Test
    void captureOutput(CapturedOutput output) {
        System.out.println("Hello World");

        assertThat(output).isEqualTo("Hello World\n");  // CapturedOutput#getAllと同じ
        assertThat(output.getAll()).isEqualTo("Hello World\n");
        assertThat(output.getOut()).isEqualTo("Hello World\n");
        assertThat(output.getErr()).isEmpty();
    }

CapturedOutput#toStringCapturedOutput#getAllと同じで、標準出力と標準エラー出力を合わせたものになります。

SLF4J+Logbackで出力したログをキャプチャーする

最後に、SLF4J+Logbackで出力したログをキャプチャーしてみましょう。こちらはSpringを使ったコードにします。

ログ出力を行うクラス。

src/main/java/org/littlewings/spring/test/PrintService.java

package org.littlewings.spring.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class PrintService {
    private Logger logger = LoggerFactory.getLogger(PrintService.class);

    public void print(String message) {
        logger.info("message: {}", message);
    }
}

テスト。

src/test/java/org/littlewings/spring/test/PrintServiceTest.java

package org.littlewings.spring.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
@SpringBootTest
class PrintServiceTest {
    @Autowired
    private PrintService printService;

    @Test
    void test(CapturedOutput output) {
        printService.print("Hello World");
        printService.print("Hello Spring");

        assertThat(output).contains("Hello World");
        assertThat(output).contains("Hello Spring");
    }
}

mvn testで出力されるログは、こんな感じですね。

[INFO] Running org.littlewings.spring.test.PrintServiceTest
23:35:24.281 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.PrintServiceTest]: PrintServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
23:35:24.363 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.PrintServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.4)

2023-10-19T23:35:24.671+09:00  INFO 27484 --- [           main] o.l.spring.test.PrintServiceTest         : Starting PrintServiceTest using Java 17.0.8.1 with PID 27484 (started by xxxxx in /path/to/output-capture-example)
2023-10-19T23:35:24.673+09:00  INFO 27484 --- [           main] o.l.spring.test.PrintServiceTest         : No active profile set, falling back to 1 default profile: "default"
2023-10-19T23:35:25.054+09:00  INFO 27484 --- [           main] o.l.spring.test.PrintServiceTest         : Started PrintServiceTest in 0.585 seconds (process running for 1.465)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2023-10-19T23:35:25.782+09:00  INFO 27484 --- [           main] o.littlewings.spring.test.PrintService   : message: Hello World
2023-10-19T23:35:25.783+09:00  INFO 27484 --- [           main] o.littlewings.spring.test.PrintService   : message: Hello Spring

こんなところでしょうか。

おわりに

Spring Bootのテストユーティリティに含まれている、OutputCaptureを試してみました。

Javadocを見るとSpring Boot 2.2.0から入っていたようですし、ドキュメントにも書かれていたのですが全然気づいていませんでした。

ログの内容を確認したい時などに便利そうなので、覚えておきましょう。