Webアプリ作成 Spring Boot
⑨テストコード Serviceクラス

JUnit5を用いてテストコードを書いていきます。
「Controllerクラス」「Serviceクラス」のテストコードを書いていきますが、
今回は「Serviceクラス」を実施していきます。

概要

テストコードとは、自分の作成したプログラムが想定通りかを確認するためのコードです。
今回はJavaで書かれたプログラムをテストするフレームワーク「JUnit5」を利用します。
テストフレームワークを利用することで、inputに対してoutputが正しいかを自動で確認することができます。
JUnitについては下記の公式ガイドを一通り読むことでだいぶ理解が深まります。

JUnit 5 ユーザーガイド

環境構築

JUnitはEclipseが古いのを利用していない限り、デフォルトで使用できるようになっているはずです。

テストでも実際にDBにアクセスしてテストということをしたいのですが、今回はテスト用のDBを新たに設定しテスト時はそこを参照するようにしていきたいと思います。
「H2データベース」を利用していきます。
H2データベースとは、JAVAプラットフォーム上でオープンソースのRDBです。
「インメモリデータベース」として使用が可能です。
Springの「Add Starters」から追加していきます。
プロジェクト上で右クリック→「Spring」→「Add Starters」を選択します。

SQL内の「H2 Database」を追加します。
「Next」を選択します。
※注意
他の利用していたものにもチェックが外れてしまうため、チェックしなおしましょう。

また、本来であればGradleの設定ファイルに追記し読み込むというやり方がいいかと思いますがこの方法で実施します

「build.gradle」を選択し、「Finish」を選択します。

設定ファイル作成

テスト用の設定ファイル、テーブル、データ作成用SQLを作成していきます。
ます「src/test/resources」フォルダを作成します。

「src/test/resources」配下に設定ファイル「application-unit.properties」を作成し、以下の内容を記載していきます。

spring.datasource.url=jdbc:h2:mem:diary;
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.schema=classpath:schema-unit.sql
spring.datasource.data=classpath:data-unit.sql

H2データベースの内容とテスト実行に自動で実行されるファイルの名称を指定しています。
自動で実行されるファイル
 ・schema.sql → テーブル定義を記載
 ・data.sql → 投入するデータを記載
 ※「-unit.sql」が該当するように設定ファイルに指定しています。

「src/test/resources」配下に設定ファイル「schema-unit.sql」を作成し、以下の内容を記載していきます。
※アプリケーション実行時の「schema.sql」と同様の内容になります。

CREATE TABLE IF NOT EXISTS diary(
  id serial,
  category varchar(2),
  title varchar(50) NOT NULL,
  content text,
  date date NOT NULL,
  update_datetime timestamp,
  PRIMARY KEY(id)
);

CREATE TABLE IF NOT EXISTS category_code(
  id serial,
  group_cd varchar(2),
  cd varchar(2),
  name varchar(20),
  PRIMARY KEY(id)
);

CREATE TABLE IF NOT EXISTS users(
  id serial,
  user_id varchar(10) NOT NULL,
  password varchar(60) NOT NULL,
  username varchar(50),
  PRIMARY KEY(id)
);

「src/test/resources」配下に設定ファイル「data-unit.sql」を作成し、以下の内容を記載していきます。テストを連続で実行した際にデータがリセットされるように先頭に「TRUNCATE」文も記載しています。

TRUNCATE TABLE diary;
TRUNCATE TABLE category_code;

INSERT INTO
  diary
VALUES
  (1, '1', '仕事', '登録しました', '2021-07-30', '2021-07-25 18:17:18.024'),
  (2, '1', '仕事', '登録しました', '2021-07-31', '2021-07-25 18:17:18.024'),
  (3, '2', '趣味', '登録しました', '2021-08-30', '2021-07-25 18:17:18.024');

INSERT INTO
  category_code
Values
  (1, '01', '1', '仕事'),
  (2, '01', '2', '趣味'),
  (3, '01', '3', 'その他')

テストコード

テストコードを書いていきます。
DiaryServiceクラスにあるメソッド全てに対して、テストコードを作成します。
JUnitでは基本的に「assert~」と記載し、想定結果と実行結果が一致しているかを確認することができます。
また@(アノテーション)を指定することで

まずは「findById」メソッドに対するメソッドのテストを記載します。
1件取得できるパターンと取得できないパターンでテストメソッドを作成します。

package diary;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import diary.entity.Diary;
import diary.service.DiaryService;

@SpringBootTest
@ActiveProfiles("unit")
public class DiaryServiceTest {

	@Autowired
	DiaryService service;
	
	@Nested
	@Sql("/data-unit.sql")
	class findById {
		@Test
		void 正常_1件取得() {
			// パラメータ設定
			int id = 1;
			// 実行
			Diary result = service.findById(id);
			// 検証
			assertAll("取得結果確認",
		            () -> assertEquals(1, result.getId(), "idが一致"),
		            () -> assertEquals("仕事", result.getTitle(), "タイトルが一致")
			);
			assertEquals(1, result.getId());
		}
		
		@Test
		void 異常_対象データなし() {
			// パラメータ設定
			int id = 99;
			// 実行
			Diary result = service.findById(id);
			// 検証
			assertNull(result);
		}
	}
}
ポイント

アノテーションをいくつか使用しております。JUnitが提供しているものSpringが提供しているもの両方を利用しています。

・JUnit提供
  @Nested
 テストクラスをクラス内に階層化しています。今回はあまりこれを利用しての恩恵は受けていないですが、階層化した内部で使えるJUnitの機能が増えます。利用している目的は明示的にどのメソッドのテストクラスかわかるようにです。この後、他メソッドのテストも記載するためどのメソッドに対してのテストかわかりやすくなります。
  @Test
   このメソッドがテストメソッドであるということを表します。

・Spring提供
  @SpringBootTest
   テストコード内でもSpring Bootの機能を有効にしてくれます。
   なのでテスト対象の「DiaryService」クラスを@Autowiredを利用してインスタンス化することができます。
  @ActiveProfiles(“unit”)
   どの設定ファイルを読み込むかを明示的に記載しています。
   テスト用に設定ファイル名を「application-unit.properties」としています。
   デフォルト名から「unit」付加しているので明治的に指定しています。
  @Sql
   テスト実行時にテスト用のデータを再度入れ直してからテストすることができます。
   なぜ入れ直す必要があるかというとデータ登録、更新、削除のテストもするためです。
   更新することによって別のテストに影響があるかもしれないため、入れ直しています。
  assertAll
   assertをまとめて内部に記載することですべてのチェックが一度に実行されて、失敗があった際にまとめて報告されます。チェック項目が複数ある場合等に使用します。
  assertEquals(1, result.getId());
   取得結果のidが「1」であることをチェックしています。
  assertNull
   取得結果が「null」であることをチェックしています。

では、実行してみましょう。
「DiaryServiceTest.java」上で右クリック→「Run As」→「JUnit Test」を選択します。

すると、JUnitタブに結果が表示されます。
全てチェックOKなのでチェックマークがついています。

ためしに、「assertEquals(1, result.getId());」の期待値を「2」に変更して実施してみましょう。
すると失敗したメソッドに✖︎がつきます。
中を見てみると「expected<2>but was<1>」と記載されています。
期待値は2だけど実際の結果は1だったよと知らせてくれています。

他のメソッド分も書いてみましょう。

package diary;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Date;
import java.util.List;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import diary.entity.Diary;
import diary.form.GetForm;
import diary.form.PostForm;
import diary.form.PutForm;
import diary.service.DiaryService;

@SpringBootTest
@ActiveProfiles("unit")
public class DiaryServiceTest {

	@Autowired
	DiaryService service;
	
	@Nested
	@Sql("/data-unit.sql")
	class findById {
		@Test
		void 正常_1件取得() {
			// パラメータ設定
			int id = 1;
			// 実行
			Diary result = service.findById(id);
			// 検証
			assertAll("取得結果確認",
		            () -> assertEquals(1, result.getId(), "idが一致"),
		            () -> assertEquals("仕事", result.getTitle(), "タイトルが一致")
			);
		}
		
		@Test
		void 異常_対象データなし() {
			// パラメータ設定
			int id = 99;
			// 実行
			Diary result = service.findById(id);
			// 検証
			assertNull(result);
		}
	}

	@Nested
	@Sql("/data-unit.sql")
	class findList {
		@Test
		void 正常_全件取得() {
			// パラメータ設定
			GetForm form = new GetForm();
			// 実行
			List<Diary> result = service.findList(form);
			// 検証
			assertEquals(3, result.size());
		}

		@Test
		void 正常_年月指定で検索() {
			// パラメータ設定
			GetForm form = new GetForm();
			form.setDate("2021/07");
			// 実行
			List<Diary> result = service.findList(form);
			// 検証
			assertEquals(2, result.size());
		}

		@Test
		void 正常_カテゴリー指定で検索() {
			// パラメータ設定
			GetForm form = new GetForm();
			form.setCategory("2");
			// 実行
			List<Diary> result = service.findList(form);
			// 検証
			assertEquals(1, result.size());
		}

		@Test
		void 正常_対象データなし() {
			// パラメータ設定
			GetForm form = new GetForm();
			form.setCategory("2");
			form.setDate("1999/07");
			// 実行
			List<Diary> result = service.findList(form);
			// 検証
			assertEquals(0, result.size());
		}
	}
	
	@Nested
	@Sql("/data-unit.sql")
	class insert {
		@Test
		void 正常_1件登録() {
			// パラメータ設定
			PostForm form = new PostForm();
			Date date = new Date();
			form.setDateForm(date);
			form.setCategoryForm("1");
			form.setTitleForm("登録テスト");
			// 実行
			int result = service.insert(form);
			// 検証
			assertEquals(1, result);
		}
		
		@Test
		void パラメータなし() {

			// 実行
			// Exceptionが発生することを確認
			assertThrows(Exception.class, () -> {
				service.insert(null);
	        });
		}
	}

	@Nested
	@Sql("/data-unit.sql")
	class update {
		@Test
		void 正常_1件更新() {
			// パラメータ設定
			PutForm form = new PutForm();
			Date date = new Date();
			form.setId(1);
			form.setDateForm(date);
			form.setCategoryForm("1");
			form.setTitleForm("更新テスト");
			// 実行
			int result = service.update(form);
			// 検証
			assertEquals(1, result);
		}
		
		@Test
		void パラメータなし() {

			// 実行
			// Exceptionが発生することを確認
			assertThrows(Exception.class, () -> {
				service.update(null);
	        });
		}
	}

	@Nested
	@Sql("/data-unit.sql")
	class delete {
		@Test
		void 正常_1件削除() {
			// パラメータ設定
			int id = 1;
			// 実行
			int result = service.delete(id);
			// 検証
			assertEquals(1, result);
		}
		
		@SuppressWarnings("null")
		@Test
		void パラメータなし() {
			// 実行
			// Exceptionが発生することを確認
			assertThrows(Exception.class, () -> {
				service.delete((Integer) null);
	        });
		}
	}
}
ポイント

findList
 検証項目としては様々な検索条件から取得できる日記の件数を検証しています。

insert,update,delete
 検証項目としては更新件数を検証しています。

assertThrows
 実行した結果、Exceptionがスローされることをチェックしています。

全部記載した上で再度テストしてみて、全て成功となることを確認しましょう。

カバレッジレポート出力

テストカバレッジのレポートを出力するために「JaCoCo」を利用してみたいと思います。
テストカバレッジ・・・テスト対象の内どれだけテストコードが網羅されているかを表す。

JaCoCoを利用するために「build.gradle」ファイルに追記します。
変更箇所は2箇所です。
plugins内に「id ‘jacoco’」を追加します。
test内にfilterを作成し、「includeTestsMatching “*.DiaryServiceTest”」を追加します。
(テスト対象のクラスを指定)

plugins {
	id 'org.springframework.boot' version '2.4.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'jacoco'
}

test {
	useJUnitPlatform()

	filter {
		includeTestsMatching "*.DiaryServiceTest"
	}
}

追加したら設定内容を反映させます。
diaryプロジェクト上で右クリック→「Gradle」→「Refresh Gradle Project」を選択します。
※数分時間がかかるかもしれません。

「Gradle Tasks」タブの「diary」→「verification」→「jacocoTestReport」を選択します。

するとこのような形で実行されます。

どこにレポートが出力されるかというと、プロジェクトフォルダ内の
「build/reports/jacoco/test/html/index.html」です。

開いて確認してみましょう。
このようにどれだけテストされているかをパーセンテージで表してくれます。

テスト対象の「DiaryService」まで開いてみましょう。

分岐がほとんどないメソッドですが、全て100%となっています。
試しみにどこかのテストをコメントアウトして再度実行してみると、内容が変わるはずです。
findByIdメソッドの一部テストをコメントアウトしてみました。
※もし変わらない場合は一度
 Gradle Tasks」タブの「diary」→「verification」→「check」を実施してみてください。

このように赤く表示され、どこかでテストされていないかがわかります。

最後に

Serviceクラスのテストを書いてみました。
テストコードを作成しておくことで、PGの変更が生じた際にテストを実施して検証することである程度品質が保たれますね。
また今回はdaoクラスなど関連するクラスもインスタンス化して実装しましたが、モック化することもできます。環境構築が面倒だったりしますが、下記でコードをのせてますので参考にしてみてください。

次回はControllerクラスのテストコードを作成してみます。

参考文献

Spring リダイレクト... - リファレンスドキュメント
Spring リダイレクト... - リファレンスドキュメント (function() { var base_url = /spring-framework/reference/ ; var id_to_url = { webtest
JUnit 5 ユーザーガイド
The JaCoCo Plugin
タイトルとURLをコピーしました