LoginSignup
2
4

More than 3 years have passed since last update.

SpringBoot + JUnit + Mockito で UnitTest

Posted at

SpringBootのプロジェクトにCIを導入したくて、自動テストについて調べていたところ、いろいろなツール名が出てきて混乱したので整理がてらメモ書きを。
自動テスト初心者の記事なので嘘ついてたらごめんなさい。わざとじゃないです。

最終的にできたプロジェクトはこちら

TL;DR

  1. JUnit でテストクラスを実行する
  2. Mockito でモックを作成して依存先を置き換える
  3. private メソッドはリフレクションでテストができる
  4. 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
    1. UserController.java
    2. UserEntity.java
    3. UserRepository.java
    4. UserService.java
  • resources
    1. application.properties
    2. 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

ファイルの内容

UserController.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!!";
    }
}
UserEntity.java
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

UserRepository.java
package com.sample.testsample;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}
UserService.java
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;
    }
}
application.properties
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
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() のテストから実装していきましょう。

UserServiceTests.java
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 現在、このテストは成功しますが問題が一つありますね。
来年になるとテストが失敗してしまいます :scream:
解決策としていくつか考えてみます...

  1. calcAge に LocalDate の引数を追加して差分を算出する
  2. 日付を取得するサービスを経由するような構成にして Interface を活用して日付を取得する
  3. 日付を取得するサービスを経由するような構成にしてモックを活用してテスト用に値を上書きする

などなど、いくつか考えられますが、 2 か 3 の手段が色々とやりやすいようですので、今回は 3 の手段を採用します。

4. Mockitoでモックを使用したテストの作成

日付取得用のサービス DateUtils.java を追加して、 UserServiceUserServiceTests を書き換えましょう

DateUtils.java
package com.sample.testsample;

import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
public class DateUtils {
    public LocalDate getNowDate() {
        return LocalDate.now();
    }
}
UserService.java
 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;
     }
 }
UserServiceTests.java
 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 クラスの依存関係をコンストラクタインジェクションで解決させること。そうすることでモックの注入がしやすくなって幸せです :blush:

さて、次に UserEntity 作成メソッドのテストを作っていきましょう。

5. privateメソッドのテスト

UserEntitiymakeUserEntity() で作成されるのですが、このメソッドのアクセス修飾子は private です。
そのままでは呼び出しができないので、リフレクションを使用してテストを作成していきましょう。

UserServiceTests にテストケースを追加しましょう。

UserServiceTests.java
 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 メソッドのテスト方法はこちらを参考にさせていただきました。大感謝! :kissing_closed_eyes:
Junitでprivateメソッドのテスト方法 - Qiita

6. テスト結果レポート

さて、現在はサンプルなのでこの程度しかテストケースがありませんが、実際のシステムはもっとたくさんのテストケースが実行されていることも多いでしょう。
テストの結果をみやすく表示するためにテスト結果のレポートを確認してみましょう.
テストを実行すると
<project_root>/build/reports/tests/test/index.html
にテスト結果のレポートが作成されていますので確認してみましょう。

7. おわりに

以上でとても簡単ですが SpringBoot アプリケーションに JUnit でのテストを組み込んでみました。
誰かのはじめの一歩のサポートになれば幸いです。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4