概要
- Spring Boot + Spring Retry によるサンプルプログラムを Spring Test で自動テストする
- テストには Spring Test + Mockito + JUnit 4 を使う
動作確認環境
- macOS Mojave
- OpenJDK 11.0.2
- Spring Boot 2.2.0 M4
- Spring Retry 1.2.4
サンプルプログラムのソースコード一覧
├── pom.xml
└── src
├── main
│ └── java
│ └── sample
│ ├── EchoController.java
│ ├── EchoRepository.java
│ └── EchoService.java
└── test
└── java
└── sample
└── EchoServiceTest.java
Maven のビルド用ファイル pom.xml
今回は Maven でビルドを実行する。
pom.xml は Spring Initializr で生成したものをベースとした。
Spring Retry を使用するため dependencies に spring-retry を追加する。また、実行時に AOP クラスが必要なため spring-boot-starter-aop を追加する。
Spring Test を使用するため spring-boot-starter-test を追加する。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.M4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>info.maigo.lab</groupId>
<artifactId>sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sample</name>
<description>Sample project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.retry/spring-retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.4.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</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>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
ソースコード
EchoController.java
Spring Boot による起動エントリクラス (@SpringBootApplication アノテーションを付加) であり、HTTPリクエストを受け取ってレスポンスを返す Controller クラス (@RestController アノテーションを付加) でもある。
Spring Retry を有効にするため @EnableRetry アノテーションを指定している。
package sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@SpringBootApplication
@EnableRetry
@RestController
public class EchoController {
public static void main(String[] args) {
SpringApplication.run(EchoController.class, args);
}
@Autowired
private EchoService service;
@RequestMapping("/{message}")
public Map<String, Object> index(@PathVariable("message") String message) {
return new HashMap<String, Object>() {
{
put("result", service.echo(message));
}
};
}
}
EchoRepository.java
Repository クラス。
package sample;
import org.springframework.stereotype.Repository;
import java.util.Random;
@Repository
public class EchoRepository {
public String echo(String message) {
// 二分の一の確率で例外が発生する
if (new Random().nextBoolean()) {
return message;
} else {
throw new RuntimeException("Failure");
}
}
}
EchoService.java
Service クラス。
Spring Retry のアノテーション @Retryable を指定したメソッド内で RuntimeException が発生した際に3回まで同じメソッドを試行する。
package sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class EchoService {
@Autowired
private EchoRepository repository;
/**
* 指定したメッセージを返します。
* たまに失敗しますが、3回まで試行します。
* @param message メッセージ
* @return メッセージ
*/
@Retryable(
value = {RuntimeException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public String echo(String message) {
System.out.println("EchoService#echo: " + message);
return repository.echo(message);
}
@Recover
public String recover(RuntimeException e, String message) {
System.out.println("EchoService#recover: " + message);
throw e;
}
}
EchoServiceTest.java
EchoService クラスのテストクラス。
EchoRepository のモックオブジェクトを生成して、EchoService の挙動をテストする。
package sample;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
@RunWith(SpringRunner.class) // Spring 用の JUnit を使う
@SpringBootTest // Spring Boot を起動してテストする
public class EchoServiceTest {
@Autowired
private EchoService service;
// ApplicationContext にモックを追加する
// (既存のオブジェクトがあれば置き換える)
@MockBean
private EchoRepository mockRepository;
@Test
public void testSuccess() {
// EchoRepository のモックを用意する
// このモックは正常に値を返す
String message = "Zero";
doReturn(message).when(mockRepository).echo(message);
// テスト
assertEquals(message, service.echo(message));
}
@Test
public void testFairureSuccess() {
// EchoRepository のモックを用意する
// このモックは例外を1回投げたあと、正常に値を返す
String message = "Uno";
doThrow(new RuntimeException("Failure"))
.doReturn(message)
.when(mockRepository).echo(message);
// テスト
assertEquals(message, service.echo(message));
}
@Test
public void testFairureFairureSuccess() {
// EchoRepository のモックを用意する
// このモックは例外を2回投げたあと、正常に値を返す
String message = "Due";
doThrow(new RuntimeException("Failure"))
.doThrow(new RuntimeException("Failure"))
.doReturn(message)
.when(mockRepository).echo(message);
// テスト
assertEquals(message, service.echo(message));
}
@Test(expected = RuntimeException.class) // RuntimeException が発生することを想定
public void testFairureFairureFairure() {
// EchoRepository のモックを用意する
// このモックは例外を3回投げる
String message = "Tre";
doThrow(new RuntimeException("Failure"))
.doThrow(new RuntimeException("Failure"))
.doThrow(new RuntimeException("Failure"))
.when(mockRepository).echo(message);
// テスト
service.echo(message);
}
}
テストを実行
mvn test コマンドを入力すると、Spring Boot が起動してテストが実行される。
$ mvn test
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< info.maigo.lab:sample >------------------------
[INFO] Building sample 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
(中略)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.M4)
(中略)
2019-07-20 20:12:34.687 INFO 25426 --- [ main] sample.EchoServiceTest : Started EchoServiceTest in 3.407 seconds (JVM running for 4.983)
EchoService#echo: Due
EchoService#echo: Due
EchoService#echo: Due
EchoService#echo: Tre
EchoService#echo: Tre
EchoService#echo: Tre
EchoService#recover: Tre
EchoService#echo: Uno
EchoService#echo: Uno
EchoService#echo: Zero
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.643 s - in sample.EchoServiceTest
2019-07-20 20:12:39.930 INFO 25426 --- [ Thread-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.650 s
[INFO] Finished at: 2019-07-20T20:12:40+09:00
[INFO] ------------------------------------------------------------------------
参考資料
- GitHub - spring-projects/spring-retry
- Testing improvements in Spring Boot 1.4
- Spring Boot Reference Documentation - 4.25. Testing
- SpringBootTest (Spring Boot Docs 2.2.0.M4 API)
- SpringRunner (Spring Framework 5.2.0.M3 API)
- org.junit (JUnit API)
- MockBean (Spring Boot Docs 2.2.0.M4 API)
- Spring Retry を利用して宣言型のリトライ処理を実装する - Qiita
- @MockBeanと@Mock、Mockito.mock()の違い | 技術系のメモ
- 心地良すぎるモックライブラリ Mockito 〜その2〜 - A Memorandum
- Mockito~doReturn/whenとwhen/thenReturnの違い | GWT Center
- Spring BootでAutowiredされるクラスをMockitoでモックする - Qiita