SpringBootのプロジェクトにCIを導入したくて、自動テストについて調べていたところ、いろいろなツール名が出てきて混乱したので整理がてらメモ書きを。
自動テスト初心者の記事なので嘘ついてたらごめんなさい。わざとじゃないです。
最終的にできたプロジェクトはこちら
TL;DR
- JUnit でテストクラスを実行する
- Mockito でモックを作成して依存先を置き換える
- private メソッドはリフレクションでテストができる
- Hamcrest はアサーション用のマッチャー
目次
1.用語
2.サンプルプロジェクトの作成
3.JUnitで単体テストの作成
4.Mockitoでモックを使用したテストの作成
5.privateメソッドのテスト
6.テスト結果レポート
7.おわりに
#1. 用語
登場する用語を簡単に並べる
- SpringBoot
- Javaのフレームワーク
- JUnit
- Javaの単体テスト向けテストフレームワーク
- 単体テストはこいつを使って実行されることが多い
- Mokito
- Javaの単体テスト向けモックフレームワーク
- テスト対象が依存するモジュールをダミーとして置き換える
- Report
- テストの実行結果をみやすく表示したhtml郡
- 依存関係
- Class A の処理で Class B を使用している場合、Class A はClass B に依存しているといいます
- カバレッジ
- 自動テストでどの程度確認できているかの指標
#2. サンプルプロジェクトの作成
なにはともあれ、テストを行うためのプロジェクトを作成します。
なんでもいいので Spring initializer で適当に作成しました。
key | val |
---|---|
Project | Gradle project |
Language | Java |
Spring Boot | 2.4.1 |
Group | com.sample |
Artifact | testsample |
Name | testsample |
Description | Sample project |
Package name | com.sample.testsample |
Packaging | Jar |
Java | 11 |
Dependencies | Spring Web |
H2 Database | |
Spring Data JPA | |
Lombok |
プロジェクトができたらよくあるユーザ登録風の処理を作成しましょう。
追加、変更したファイルは以下
- testsample
- UserController.java
- UserEntity.java
- UserRepository.java
- UserService.java
- resources
- application.properties
- schema.sql
src部分のディレクトリ構成はこんな感じ
src
├── main
│ ├── java
│ │ └── com
│ │ └── sample
│ │ └── testsample
│ │ ├── TestsampleApplication.java
│ │ ├── UserController.java
│ │ ├── UserEntity.java
│ │ ├── UserRepository.java
│ │ └── UserService.java
│ └── resources
│ ├── application.properties
│ ├── schema.sql
│ ├── static
│ └── templates
└── test
└── java
└── com
└── sample
└── testsample
└── TestsampleApplicationTests.java
ファイルの内容
package com.sample.testsample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
@RestController
public class UserController {
@Autowired
UserService userService;
@RequestMapping(value="/index", method = RequestMethod.GET)
public String index() {
return "This page is user page";
}
@RequestMapping(value="/add", method = RequestMethod.POST)
public String add(@RequestParam String name,
@DateTimeFormat(pattern = "yyyy-MM-dd")
@RequestParam LocalDate birthday) {
userService.addUser(name, birthday);
return "Success!!";
}
}
package com.sample.testsample;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private Long age;
}
※ Lombok の @Data
が動かない場合、手動でアクセサを追加してもOK
package com.sample.testsample;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}
package com.sample.testsample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public void addUser(String name, LocalDate birthday) {
// User entity の作成
UserEntity entity = makeUserEntity(name, birthday);
// 保存
userRepository.save(entity);
}
private UserEntity makeUserEntity(String name, LocalDate birthday) {
// User entity の作成
UserEntity entity = new UserEntity();
entity.setName(name);
// 年齢の算出・設定
LocalDate now = LocalDate.now();
Long age = ChronoUnit.YEARS.between(birthday, now);
entity.setAge(age);
return entity;
}
}
spring.datasource.url=jdbc:h2:./test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=testsample
spring.datasource.password=testsample
spring.datasource.sql-script-encoding=UTF-8
spring.datasource.initialization-mode=always
spring.datasource.schema=classpath:schema.sql
DROP TABLE user;
CREATE TABLE user (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(256) NOT NULL,
age INTEGER NOT NULL,
PRIMARY KEY (id)
);
さて、ここまでで登録処理ができたので次にテストを作っていきましょう
#3. JUnitで単体テストの作成
テストクラスは test
パッケージ配下に xxxTests.java の命名規則で作成していきます。
今回の場合ロジックは UserService.java
にあるので、これをテストしていきたいので UserServiceTests.java
を追加します。
まずは簡単にテストできそうな calckAge()
のテストから実装していきましょう。
package com.sample.testsample;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
public class UserServiceTests {
@Test
public void 年齢算出のテスト() {
UserService service = new UserService(null);
LocalDate date = LocalDate.of(2000, 01, 01);
Long age = service.calcAge(date);
Assertions.assertEquals(21l, age);
}
}
2021/1/11 現在、このテストは成功しますが問題が一つありますね。
来年になるとテストが失敗してしまいます
解決策としていくつか考えてみます...
- calcAge に LocalDate の引数を追加して差分を算出する
- 日付を取得するサービスを経由するような構成にして Interface を活用して日付を取得する
- 日付を取得するサービスを経由するような構成にしてモックを活用してテスト用に値を上書きする
などなど、いくつか考えられますが、 2 か 3 の手段が色々とやりやすいようですので、今回は 3 の手段を採用します。
#4. Mockitoでモックを使用したテストの作成
日付取得用のサービス DateUtils.java
を追加して、 UserService
と UserServiceTests
を書き換えましょう
package com.sample.testsample;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
public class DateUtils {
public LocalDate getNowDate() {
return LocalDate.now();
}
}
package com.sample.testsample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
@Service
public class UserService {
+ private final DateUtils dateUtils;
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository
+ , DateUtils dateUtils) {
this.userRepository = userRepository;
+ this.dateUtils = dateUtils;
}
...
public Long calcAge(LocalDate birthday) {
- LocalDate now = LocalDate.now();
- Long age = ChronoUnit.YEARS.between(birthday, now);
+ Long age = ChronoUnit.YEARS.between(birthday, dateUtils.getNowDate());
return age;
}
}
package com.sample.testsample;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import java.time.LocalDate;
public class UserServiceTests {
@Test
public void 年齢算出のテスト() {
+ // Mockの作成
+ DateUtils dateUtils = Mockito.mock(DateUtils.class);
+ Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
- UserService service = new UserService(null);
+ UserService service = new UserService(null, dateUtils);
LocalDate date = LocalDate.of(2000, 01, 01);
Long age = service.calcAge(date);
Assertions.assertEquals(21l, age);
}
}
Mockito.mock()
で DateUtils
と同じメソッドを持ったモックを作成します。
ただし、モックですので呼び出したメソッドは null を返却してしまいます。
そこで Mockito.when()
と thenReturn()
を使って dateUtils.getNowDate()
の戻り値を上書きしてやります。
そうすることでこのテストは現在日付によらず成功を確認できるわけですね。
ポイントとしては UserService
クラスの依存関係をコンストラクタインジェクションで解決させること。そうすることでモックの注入がしやすくなって幸せです
さて、次に UserEntity
作成メソッドのテストを作っていきましょう。
#5. privateメソッドのテスト
UserEntitiy
は makeUserEntity()
で作成されるのですが、このメソッドのアクセス修飾子は private です。
そのままでは呼び出しができないので、リフレクションを使用してテストを作成していきましょう。
UserServiceTests
にテストケースを追加しましょう。
public class UserServiceTests {
/* write other test cases */
+ @Test
+ public void ユーザエンティティ作成のテスト() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ // Mockの作成
+ DateUtils dateUtils = Mockito.mock(DateUtils.class);
+ Mockito.when(dateUtils.getNowDate()).thenReturn(LocalDate.of(2021, 1, 11));
+ // reflection で private メソッドを取得
+ UserService service = new UserService(null, dateUtils);
+ Method method = service.getClass().getDeclaredMethod("makeUserEntity", String.class, LocalDate.class);
+ method.setAccessible(true);
+ UserEntity entity = (UserEntity) method.invoke(service, "Richter", LocalDate.of(2000, 1, 1));
+ // 結果の比較
+ Assertions.assertEquals(null, entity.getId());
+ Assertions.assertEquals("Richter", entity.getName());
+ Assertions.assertEquals(21l, entity.getAge());
+ }
}
private メソッドのテスト方法はこちらを参考にさせていただきました。大感謝!
Junitでprivateメソッドのテスト方法 - Qiita
#6. テスト結果レポート
さて、現在はサンプルなのでこの程度しかテストケースがありませんが、実際のシステムはもっとたくさんのテストケースが実行されていることも多いでしょう。
テストの結果をみやすく表示するためにテスト結果のレポートを確認してみましょう.
テストを実行すると
<project_root>/build/reports/tests/test/index.html
にテスト結果のレポートが作成されていますので確認してみましょう。
#7. おわりに
以上でとても簡単ですが SpringBoot アプリケーションに JUnit でのテストを組み込んでみました。
誰かのはじめの一歩のサポートになれば幸いです。