これは、なにをしたくて書いたもの?
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
の場合は設定のリセットが必要だとOutputCaptureExtension
のJavadocに書かれています。
@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#toString
はCapturedOutput#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から入っていたようですし、ドキュメントにも書かれていたのですが全然気づいていませんでした。
ログの内容を確認したい時などに便利そうなので、覚えておきましょう。