20210506のJavaに関する記事は13件です。

手軽にMicrosoft Build of OpenJDKを触る方法

2021年4月6日にMicrosoftがOpenJDKをビルドして提供するという発表がありました。以下、主な情報です。 Bruno Borgesさんによる記事「Announcing Preview of Microsoft Build of OpenJDK」 「Microsoft Build of OpenJDK」のページ Microsoft Build of OpenJDKを試すには、上記のページからJDKをダウンロードしてインストールする他、AzureのCloud Shellを使う方法があります。現時点ではDockerイメージが提供されていないため、Cloud Shellを使うのが一番手軽な気がします。 以下動画のようにデフォルトでインストールされています。mavenも入っているので(前から入ってましたっけ...?)簡単なプログラムであればCloud Shell上でも作れそうです。もちろんJShellも動くので、ちょっとした学習で使う分には(インストールの手間もかからず)便利だと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】Enumを使って変更に強いコードを書こう!

概要 仕事でコード値の判定が色んなところに散らばってしまった後に、コード値の定義を変更しなければいけなくなった状況が起こりました。 Enumで定義していれば変更箇所はEnum定義だけでよかったなぁ~と思い、自戒の意味を込めて執筆致しました。 状況 「なんちゃら種別」のような種別コード値と名称が複数定義されている その定義の中に、「3:その他」が最後に定義されていた。 新たに追加されることになった種別コードは「3」を使いたい。 そのため、「その他」は今後さらにコード値が追加されることを考慮して、 「99:その他」にしたい。 実装 問題のあるコード SalesOrderMain.java アプリケーションのエントリーポイント(実行開始メソッド) SalesOrderMain.java package main.enum_case; public class SalesOrderMain { public static void main(String...strings) { SalesOrder otherOrder = new SalesOrder("販売停止商品", 0, 3); // OTHER=3をハードコーディングで判定する場合 // ⇒ ロジックは通るが、コード値を修正したらif文を修正する必要がある if(3 == otherOrder.saleKindCode()) { System.out.println("コード値3:販売停止商品です。[ハードコーディング]"); } // OTHERをEnumで判定していた場合 // ⇒ コードを変更することなく動作する if(SalesKind.OTHER.code() == otherOrder.saleKindCode()) { System.out.println("コード値" + SalesKind.OTHER.code()+ ":販売停止商品です。[Enum定義]"); } } } 実行結果 一応ハードコーティングとEnum定義での両方でロジックが通っています。 コード値3:販売停止商品です。[ハードコーディング] コード値3:販売停止商品です。[Enum定義] if(3 == otherOrder.saleKindCode()) しかしここがハードコーディングになっているので、コード値の修正に対応していません。 SalesOrder.java 売上種別コードを持つオブジェクト SalesOrder.java package main.enum_case; public class SalesOrder { private String _name; private Integer _amount; private Integer _salesKindCode; SalesOrder(String name, Integer amount, Integer salesKindCode){ _name = name; _amount = amount; _salesKindCode = salesKindCode; } public Integer saleKindCode() { return _salesKindCode; } } SalesKind.java 販売種別=3(その他)が定義されている状態 SalesKind.java package main.enum_case; public enum SalesKind { A(1, "A"), B(2, "B"), OTHER(3, "OTHER"); private Integer _code; private String _name; private SalesKind(Integer code, String name) { _code = code; _name = name; } public Integer code() { return _code; } // Enumで定義されているnameメソッドを重複すため、nameのプロパティはofNameとする。 public String ofName() { return _name; } }   Enum定義で解消したコード SalesOrderMain.java アプリケーションのエントリーポイント(実行開始メソッド) SalesOrderのインスタンス生成時の売上種別を99に変更しています。 SalesOrderMain.java package main.enum_case; public class SalesOrderMain { public static void main(String...strings) { SalesOrder otherOrder = new SalesOrder("販売停止商品", 0, 99); // 99に変更 // OTHER=3をハードコーディングで判定する場合 // ⇒ ロジックは通るが、コード値を修正したらif文を修正する必要がある if(3 == otherOrder.saleKindCode()) { System.out.println("コード値3:販売停止商品です。[ハードコーディング]"); } // OTHERをEnumで判定していた場合 // ⇒ コードを変更することなく動作する if(SalesKind.OTHER.code() == otherOrder.saleKindCode()) { System.out.println("コード値" + SalesKind.OTHER.code()+ ":販売停止商品です。[Enum定義]"); } } }   実行結果 Enum定義で判定していたロジックのみ通っています。 コード値99:販売停止商品です。[Enum定義] if(3 == otherOrder.saleKindCode()) ここはハードコーディングしていたため、修正しなくてはならなくなりました。 SalesOrder.java 売上種別コードを持つオブジェクト 修正箇所は無いため割愛 SalesKind.java 販売種別=99(その他)に定義し直した状態 SalesKind.java package main.enum_case; public enum SalesKind { A(1, "A"), B(2, "B"), C(3, "C"), // OTHER(3, "OTHER"); // 元々は3だったが、3が追加されたために99に変更することになった OTHER(99, "OTHER"); private Integer _code; private String _name; private SalesKind(Integer code, String name) { _code = code; _name = name; } public Integer code() { return _code; } // Enumで定義されているnameメソッドを重複すため、nameのプロパティはofNameとする。 public String ofName() { return _name; } } まとめ コード値の判定ロジックはEnum定義などを利用し、変更に強い構造にしておくこと。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring-Boot の REST API の仕様書を Springfox 3 から Swagger で生成する (Maven)

環境 Java 11 mvn 準備 ひながた Spring Initializaer からWEBアプリを落としてくる APIの口を書く DemoApplication に直接書いてもいいけどちゃんと独立したクラスにしましょう。 MyController.java package com.example.demo; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class MyController { @GetMapping("/hoge") public String hoge(){ return "hoge"; } } Springfox の追加 https://github.com/springfox/springfox にあるように、以下の依存関係をMavenに追加する。「dependency」で探せばそれっぽいところがみつかるはず。 pom.xml <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> 起動してみる なんとこれだけでうごくのである。 コマンドラインとか、Eclipse とか使ってる人は右クリックとか、とにかく mvn spring-boot:run を実行すると、(何かでポートが使われてなければ)8080番でサーバが起動する。 http://localhost:8080/hoge でAPIの応答が確認できる。 http://localhost:8080/swagger-ui/ に行けば以下のような画面が見れる筈。 ちゃんとクラス名まで拾ってくれる。尚API個別の説明とかは Springfox2 のアノテーションが使えるみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MicronautのWebアプリケーションをAzure App Serviceで動かす

Azure App ServiceでSpring Boot以外にどんなWebフレームワークが動かせるのか?これまでいくつか試してきました。 - HelidonのWebアプリケーションをAzure App Serviceで動かす - QuarkusのWebアプリケーションをAzure App Serviceで動かす - Vert.xのWebアプリケーションをAzure App Serviceで動かす 今回はMicronautです。最初にMicronaut Launchでプロジェクトを作成しました。選択した項目は以下のとおりです。 ローカル環境での実行 Micronaut Launchで作成したプロジェクトを解凍して、こちらのドキュメントに従ってHelloControllerを追加します。 package com.example; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; @Controller("/hello") public class HelloController { @Get(produces = MediaType.TEXT_PLAIN) public String index() { return "Hello World"; } } ビルドします。 mvn clean package 実行します。 java -jar target/*.jar 起動しました。 localhost:8080にアクセスすると以下のような表示になります。 localhost:8080/helloにアクセスするとHelloControllerの結果が返ります。 これをAzure App Serviceにデプロイしていきます。 Azureでの実行 最初にAzure CLIでログインしておきます。 az login 次にpom.xmlのbuildセクションに以下を追加します。 <plugin> <groupId>com.microsoft.azure</groupId> <artifactId>azure-webapp-maven-plugin</artifactId> <version>1.14.0</version> </plugin> 以下コマンドを実行します。 mvn com.microsoft.azure:azure-webapp-maven-plugin:1.14.0:config 今回はlinux、Java 11を選びました。 ビルドしてデプロイします。 mvn clean package mvn com.microsoft.azure:azure-webapp-maven-plugin:1.14.0:deploy デプロイできたら実際にアクセスしてみます。 問題なく表示されました。色々なJava Webフレームワークを試してきましたが、ほとんどのものをApp Service上で動かせる気がします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaで「FactoryBot」のようものを実装してみる。

はじめに Rubyのgemの「FactoryBot」ですが、超有名プロダクトであることは衆目の一致するところです。 旧名称は「FactoryGirl」でしたので、こちらの方がピンとくる方も多いと思います。 テスト実装するときに、モックが返却する結果は手作業で実装することがほとんどだと思います。この実装が面倒ですよね。簡単な回避方法としては、共通化して、使いまわすぐらいでしょうか。 このテストデータの作り方の違いによって、テストが成功したり、失敗したりすることがあります。そんな場合は、テストの実装方法やプロダクトコードに問題がある場合が大半なのですが、せっかく実装したテストがテストデータ依存って、こんな悲しいことはありません。 テストを実行するたびに、新しいテストデータが生成され、テストデータ依存にならないテストが実装できればなーー、となります。 そんなときは「Factory Bot」です! ってJavaなんですけど・・・ ・・・ モックの戻り値の生成であれば、文字列の長さはそれほど問題にはなりませんが、DBに前提データとしてセットする場合は、長さの上限も重要となります。 完成版の全ソースは GitHubリポジトリ に登録しております。 目的 以下を実現することを目的とします。 ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成ロジックの実装 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 利用するライブラリ JSqlParser Create TableのSQLを解析し、character varyingのようなデータタイプのカラム長を取得する用途で利用します。 JUnit5とAssertJ テストでJUnit5を利用します。 具体的には、junit-jupiter-api、junit-jupiter-engine、junit-jupiter-paramsを利用します。 「SnakeYAML」 YAMLを読み込むために利用します。 「Apache Commons Text」と「Apache Commons Lang 3」 ランダムな値の生成で利用します。 具体的には、 org.apache.commons.text.RandomStringGenerator org.apache.commons.lang3.RandomUtils を利用します。 開発環境 JDK11、Gradle、IDEですが、JavaのGradleプロジェクトが利用可能なものであればOKです。 IntelliJとEclipseでテストが通ることを確認済みです。 build.gradleは以下のようになります。 plugins { id 'java' } group 'jp.small_java_world' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.7.0' implementation group: 'org.yaml', name: 'snakeyaml', version: '1.28' implementation group: 'com.github.jsqlparser', name: 'jsqlparser', version: '4.0' implementation 'org.slf4j:slf4j-api:1.7.30' implementation 'ch.qos.logback:logback-core:1.2.3' implementation 'ch.qos.logback:logback-classic:1.2.3' implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' testCompile group: 'org.assertj', name: 'assertj-core', version: '3.14.0' testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.9.0' testImplementation group: 'org.mockito', name: 'mockito-inline', version: '3.9.0' } test { useJUnitPlatform() } ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成には、ランダムな値を生成するロジックが必要ですので、まずはここからはじめたいと思います。 RandomDataUtil package jp.small_java_world.dummydatafactory.util; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.text.RandomStringGenerator; public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } } 重複チェックのための各種Set RandomDataUtil static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); 生成した値が重複していないかを確認するための各種Setとなります。 generateRandomString ランダムな長さ指定の文字列生成メソッド RandomDataUtil public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } generateRandomLetterOrDigitをstart = '0', end = 'z'で呼び出し、結果がrandomStringCheckMapに存在しない場合は、その値を結果として返却します。 RandomDataUtil private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } generateRandomLetterOrDigitですが、charのレンジ指定(ASCIIコード)で生成した文字列を数値とアルファベットだけにし、長さ指定の文字列を返却します。 start = '0', end = 'z'の場合 '0'はASCIIコードで48(10進数)、'z'は同じく122(10進数)ですので、 ASCIIコードが91である'['も含みますので、filteredBy(Character::isLetterOrDigit)を指定して数値とアルファベットのみにフィルターしています。 generateRandomStringのテスト RandomDataUtilTestは初登場ですので、packageから張り付けております。 中身は、testGenerateRandomStringの関連メソッドだけにしております。 package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; class RandomDataUtilTest { // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } private void assertContainChar(String target, char startChar, char endChar) { assertContainChar(target, startChar, endChar, null); } private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } } testGenerateRandomStringメソッドですが、ParameterizedTestのValueSource指定となっており、RandomDataUtil.generateRandomStringの呼び出し時に指定するlengthのバリエーションテストとなります。 @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } assertContainCharは、生成したランダムな文字列が期待する文字種のみで構成されていることを検証するメソッドとなります。 先ほども説明させていただきましたが、ASCIIコードのレンジでの判定だけだと、記号も含まれますので、レンジ内の除外コードのリストを第三引数で指定可能となります。 private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); generateRandomHexString ランダムな長さ指定の16進数の文字列生成メソッド '0', 'f'のレンジだと、アルファベットの大文字小文字が含まれるので、大文字だけにするために final char start = '0', end = 'F';としております。 先頭が0になってしまうと、lengthの指定が無意味になってしまいますので、先頭の文字と2文字目以降の文字列を別々に生成しています。 RandomDataUtil public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomHexStringのテスト RandomDataUtil.generateRandomHexString(length)の結果は、ランダムな16進文字列ですので、 全体が'0', 'F'のレンジで、EXCLUDE_LETTER_OR_DIGIT_LISTを含まない、 先頭が'1', 'F'のレンジでEXCLUDE_LETTER_OR_DIGIT_LISTを含まない との検証を行っています。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomHexString(int length) { var result1 = RandomDataUtil.generateRandomHexString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'F'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result1.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomHexString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result2.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomNumberString ランダムな長さ指定の10進数の文字列生成メソッド generateRandomHexStringとよく似ています。全体のレンジが'1', '9'になっている部分だけ異なります。 RandomDataUtil public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomNumberStringのテスト 全体のレンジが'0', '9'ですので、EXCLUDE_LETTER_OR_DIGIT_LISTは考慮不要となります。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomNumberString(int length) { var result1 = RandomDataUtil.generateRandomNumberString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'9'の間に含まれていることを検証 assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result1.substring(0, 1), '1', '9'); var result2 = RandomDataUtil.generateRandomNumberString(length); assertThat(result2).hasSize(length); assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result2.substring(0, 1), '1', '9'); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomDate ランダムな値のjava.util.Date生成メソッド 現在のシステム時刻±365日の範囲内のDateを生成します。 RandomDataUtil public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } generateRandomDateのテスト RandomDataUtil#generateRandomTimestamp()がまだ説明できていないのですが、generateRandomDateとgenerateRandomTimestamp両方のテストとなります。 ParameterizedTestのEnumSource指定で、generateRandomDateとgenerateRandomTimestampの両方を対象とするテストとして実装しております。 RandomDataUtilTest private enum RandomDateTestType { DATE, TIMESTAMP; } @ParameterizedTest @EnumSource(RandomDateTestType.class) void testGenerateRandomDate(RandomDateTestType testType) { // 期待値の上限=now+365dayに10秒を足したDate // Calendar.getInstance()をテストで呼び出すタイミングと // RandomDataUtil.generateRandomDate()で呼び出すタイミングに時差があるので // 時差でテストが期待した動作にならずテストが失敗するパターンを排除するために10秒の猶予を設けています。 var calendarMax = Calendar.getInstance(); calendarMax.add(Calendar.DATE, 365); calendarMax.add(Calendar.SECOND, 10); var maxDate = calendarMax.getTime(); // 期待値の下限=now+365dayに-10秒を足したDate var calendarMin = Calendar.getInstance(); calendarMin.add(Calendar.DATE, -365); calendarMin.add(Calendar.SECOND, -10); var minDate = calendarMin.getTime(); Date result1 = null; if (testType == RandomDateTestType.DATE) { result1 = RandomDataUtil.generateRandomDate(); assertFalse(result1 instanceof Timestamp); } else if (testType == RandomDateTestType.TIMESTAMP) { result1 = RandomDataUtil.generateRandomTimestamp(); assertTrue(result1 instanceof Timestamp); } // calendarMaxの方がresult1より未来 assertTrue(maxDate.compareTo(result1) > 0); // calendarMinの方がresult1より過去 assertTrue(result1.compareTo(minDate) > 0); Date result2 = null; if (testType == RandomDateTestType.DATE) { result2 = RandomDataUtil.generateRandomDate(); } else if (testType == RandomDateTestType.TIMESTAMP) { result2 = RandomDataUtil.generateRandomTimestamp(); } // calendarMaxの方がresult2より未来 assertTrue(maxDate.compareTo(result2) > 0); // calendarMinの方がresult2より過去 assertTrue(result2.compareTo(minDate) > 0); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomTimestamp ランダムな値のjava.sql.Timestamp生成メソッド RandomDataUtil#generateRandomDate()の結果をTimestampに変換しているだけとなります。 RandomDataUtil public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } booleanと数値型のランダムな値生成メソッド RandomDataUtil public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } 数値型のランダムな値生成メソッドのテスト テストの見た目が貧弱なので、3回呼び出し、varで結果を受けているのでビジュアル的な訴求力が乏しいので、instanceofで型の確認を行っています。 RandomDataUtilTest @Test void testGenerateRandomInt() { var result1 = RandomDataUtil.generateRandomInt(); var result2 = RandomDataUtil.generateRandomInt(); var result3 = RandomDataUtil.generateRandomInt(); assertTrue(result1 instanceof Integer); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } @Test void testGenerateRandomLong() { var result1 = RandomDataUtil.generateRandomLong(); var result2 = RandomDataUtil.generateRandomLong(); var result3 = RandomDataUtil.generateRandomLong(); assertTrue(result1 instanceof Long); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } @Test void testGenerateRandomShort() { var result1 = RandomDataUtil.generateRandomShort(); var result2 = RandomDataUtil.generateRandomShort(); var result3 = RandomDataUtil.generateRandomShort(); assertTrue(result1 instanceof Short); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } RandomDataUtilのリファクタリング while (true)の無限ループは問題です。短い長さを指定した場合や、一気に大量のテストケースを流した場合には、文字通り無限ループにはまります。 また、文字列系の生成メソッド(generateRandomString、generateRandomHexString、generateRandomNumberString)がびしょびしょ(!DRY)ですので、少しきれいにしてみます。 RandomDataUtil public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); static final int MAX_RETRY = 10; public static String generateRandomString(final int length) { return generateRandomString(length, '0', 'z', (char) 0); } public static String generateRandomHexString(final int length) { return generateRandomString(length, '0', 'F', '1'); } public static String generateRandomNumberString(final int length) { return generateRandomString(length, '0', '9', '1'); } public static String generateRandomString(final int length, final char start, final char end, final char firstStart) { int retryCount = 0; String firstResult = "", result = ""; while (true) { // firstStartがasciiコードの0でなければ先頭の1文字目と2文字目以降の生成を別に行う。 if (firstStart != 0) { firstResult = generateRandomLetterOrDigit(firstStart, end, 1); result = length == 1 ? firstResult : firstResult + generateRandomLetterOrDigit(start, end, length - 1); } else { result = generateRandomLetterOrDigit(start, end, length); } if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { int retryCount = 0; while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } if (retryCount++ > MAX_RETRY) { return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { int retryCount = 0; while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Integer generateRandomInt() { int retryCount = 0; while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Long generateRandomLong() { int retryCount = 0; while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { int retryCount = 0; while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } } リファクタリングと無限ループ回避(ってこれやっちゃいけないパターンですが・・・)後でもRandomDataUtilTestが問題なく成功するはずです。 「ランダムな値を含むインスタンスの生成ロジックの実装」は、これで完成となります。 ランダムな値を含むインスタンスの生成ロジックの実装 DummyDataFactory#generateDummyInstance ランダムな値を含むインスタンスの生成メソッドDummyDataFactory#generateDummyInstance(Class)の説明をさせていただきます。 DummyDataFactory package jp.small_java_world.dummydatafactory; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } まずは、 DummyDataFactory // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } で生成対象のクラスとその親クラスに存在するメンバの配列(Field[] fields)を準備しています。 DummyDataFactory // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); 次に、生成対象のクラスのインスタンスの作成を行います。 最後に、fieldsをループし、RandomValueGenerator.generateRandomValue(type)でダミー値を生成、インスタンス(entity)に セットとなります。finalなフィールドは変更できませんので、無視しています。 DummyDataFactory // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } DummyDataFactory#generateDummyInstanceのテスト RandomValueGenerator.generateRandomValue(type)の説明の前に、DummyDataFactory#generateDummyInstanceのテストの説明をさせていただきます。 DummyEntityクラスを対象とし、ダミーインスタンスが正しく生成されることを確認しています。 DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class DummyDataFactoryTest { @Test void testGenerateDummyInstance() throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()), times(1)); } } DummyEntity package jp.small_java_world.dummydatafactory.entity; public class DummyEntity { int integerMember; short shortMember; public int getIntegerMember() { return integerMember; } public short getShortMember() { return shortMember; } public void setIntegerMember(int integerMember) { this.integerMember = integerMember; } public void setShortMember(short shortMember) { this.shortMember = shortMember; } } まずは、DummyEntityクラスのフィールドを取得しています。 DummyDataFactoryTest var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); ReflectUtilは、以下のようになります。 ReflectUtil package jp.small_java_world.dummydatafactory.util; import java.lang.reflect.Field; public class ReflectUtil { public static void setStaticFieldValue(Class<?> targetClass, String fieldName, Object value) throws Exception { Field targetField = getDeclaredField(targetClass, fieldName); targetField.setAccessible(true); targetField.set(null, value); } public static void setFieldValue(Object targetObject, String fieldName, Object setValue) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); targetField.set(targetObject, setValue); } public static Object getFieldValue(Object targetObject, String fieldName) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); return targetField.get(targetObject); } public static Field getDeclaredField(Class<?> originalTargetClass, String fieldName) throws NoSuchFieldException { Class<?> targetClass = originalTargetClass; Field targetField = null; while (targetClass != null) { try { targetField = targetClass.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { targetClass = targetClass.getSuperclass(); } } return targetField; } } RandomValueGeneratorをモック化するために DummyDataFactoryTest try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { でrandomValueGeneratorMockを生成しています。RandomValueGenerator.generateRandomValueはstaticメソッドですので、Mockito.mockStaticを利用する必要があります。 randomValueGeneratorMockの振る舞いを定義します。 DummyDataFactoryTest // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()); }).thenReturn((short) 101); これでDummyDataFactory.generateDummyInstance(DummyEntity.class)の結果が固定されますので DummyDataFactoryTest // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); と生成されたインスタンスの値を検証します。 最後に、randomValueGeneratorMockの振る舞いのverifyです。 DummyDataFactoryTest // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()), times(1)); RandomValueGenerator#generateRandomValue RandomValueGenerator#generateRandomValue(type)は以下のようになります。 RandomValueGenerator package jp.small_java_world.dummydatafactory; import java.sql.Timestamp; import java.util.Date; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; public class RandomValueGenerator { public static final int DAFAULT_DATA_SIZE = 10; public static Object generateRandomValue(Class<?> type) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil.generateRandomString(DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } } if (type.isAssignableFrom(String.class)) { のようにtypeで分岐し、RandomDataUtilのtypeに対応するメソッドを呼び出し値を返却しています。 現時点では、Stringのサイズは10固定としています。 Stringはプリミティブ型が存在しないのでこれでOKなのですが、Integerのようにプリミティブ型が存在する場合は、以下のようにtype.getName().equals("int")のor条件が必要となります。 } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { RandomValueGenerator#generateRandomValueのテスト RandomValueGeneratorTargetDtoを対象クラスとするテストとなります。 RandomValueGeneratorTest package jp.small_java_world.dummydatafactory; import static org.junit.jupiter.api.Assertions.assertEquals; import java.sql.Timestamp; import java.util.Calendar; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.RandomValueGeneratorTargetDto; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class RandomValueGeneratorTest { private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberLong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { var targetField = ReflectUtil.getDeclaredField(RandomValueGeneratorTargetDto.class, testType.targetMemberName); try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomString(RandomValueGenerator.DAFAULT_DATA_SIZE); }).thenReturn(GenerateRandomValueTestType.STRING.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomLong(); }).thenReturn(testType == GenerateRandomValueTestType.LONG ? GenerateRandomValueTestType.LONG.mockResult : GenerateRandomValueTestType.PRIMITIVE_LONG.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomFloat(); }).thenReturn(testType == GenerateRandomValueTestType.FLOAT ? GenerateRandomValueTestType.FLOAT.mockResult : GenerateRandomValueTestType.PRIMITIVE_FLOAT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomShort(); }).thenReturn(testType == GenerateRandomValueTestType.SHORT ? GenerateRandomValueTestType.SHORT.mockResult : GenerateRandomValueTestType.PRIMITIVE_SHORT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomBool(); }).thenReturn( testType == GenerateRandomValueTestType.BOOLEAN ? GenerateRandomValueTestType.BOOLEAN.mockResult : GenerateRandomValueTestType.PRIMITIVE_BOOLEAN.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomDate(); }).thenReturn(GenerateRandomValueTestType.DATE.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomTimestamp(); }).thenReturn(GenerateRandomValueTestType.TIMESTAMP.mockResult); var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); } } } RandomValueGeneratorTargetDto package jp.small_java_world.dummydatafactory.entity; import java.sql.Timestamp; import java.util.Date; public class RandomValueGeneratorTargetDto { String memberString; Integer memberInteger; int memberInt; Long memberLong; long memberlong; Float memberFloat; float memberfloat; Short memberShort; short membershort; Boolean memberBoolean; boolean memberboolean; Date memberDate; Timestamp memberTimestamp; public String getMemberString() { return memberString; } public void setMemberString(String memberString) { this.memberString = memberString; } public Integer getMemberInteger() { return memberInteger; } public void setMemberInteger(Integer memberInteger) { this.memberInteger = memberInteger; } public int getMemberInt() { return memberInt; } public void setMemberInt(int memberInt) { this.memberInt = memberInt; } public Long getMemberLong() { return memberLong; } public void setMemberLong(Long memberLong) { this.memberLong = memberLong; } public long getMemberlong() { return memberlong; } public void setMemberlong(long memberlong) { this.memberlong = memberlong; } public Float getMemberFloat() { return memberFloat; } public void setMemberFloat(Float memberFloat) { this.memberFloat = memberFloat; } public float getMemberfloat() { return memberfloat; } public void setMemberfloat(float memberfloat) { this.memberfloat = memberfloat; } public Short getMemberShort() { return memberShort; } public void setMemberShort(Short memberShort) { this.memberShort = memberShort; } public short getMembershort() { return membershort; } public void setMembershort(short membershort) { this.membershort = membershort; } public Boolean getMemberBoolean() { return memberBoolean; } public void setMemberBoolean(Boolean memberBoolean) { this.memberBoolean = memberBoolean; } public boolean isMemberboolean() { return memberboolean; } public void setMemberboolean(boolean memberboolean) { this.memberboolean = memberboolean; } public Date getMemberDate() { return memberDate; } public Timestamp getMemberTimestamp() { return memberTimestamp; } public void setMemberDate(Date memberDate) { this.memberDate = memberDate; } public void setMemberTimestamp(Timestamp memberTimestamp) { this.memberTimestamp = memberTimestamp; } } テストメソッドtestGenerateRandomValueは、ParameterizedTestのEnumSource指定となります。 RandomValueGeneratorTest @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { GenerateRandomValueTestTypeは、RandomValueGeneratorTargetDtoの各メンバの名前と RandomValueGenerator.generateRandomValue(type:各メンバに対応する型); が呼びされた時の期待値を管理するenumとなります。 RandomValueGeneratorTest private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberLong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } RandomValueGeneratorTest try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { でRandomDataUtilをモック化し、GenerateRandomValueTestType testTypeのtargetMemberNameとmockResultでモックの振る舞いを定義し、 RandomValueGeneratorTest var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); でRandomValueGenerator.generateRandomValue(targetField.getType())の結果がtestTypeのmockResultと一致することを検証しています。 本テストでは、モックのverifyは行っていませんので、現在のテスト対象のEnum以外のモックの振る舞いも定義してますが RandomValueGeneratorTest randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); のようにRandomDataUtilの同一メソッドの振る舞いの分岐を行う必要があります。 これで「ランダムな値を含むインスタンスの生成ロジックの実装」は完了となります。 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 Create TableのSQLを解析し、JavaのString型のメンバの長さを決定する必要があります。 SQLのパスを特定 SQLを解析し、長さを特定 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 を実現していきます。 ロジックの実装に必要な処理の実装 Javaのプロジェクト/bin/main/パスから上位に上がって指定ディレクトリを探す処理 Create TableのSQLはバージョン管理システムで管理しており、ローカルのチェックアウト先のファイルをそのまま利用することを想定しております。テスト用のパスにコピーして利用では変更に弱いですので。 プロジェクト名がDummyDataFactoryの場合で、以下のようなディレクトリ構造の場合 DummyDataFactory/bin/main/  ├ ../../hoge1  ├ ../../../hoge2/fuga DirectoryUtil.getPath("hoge1") だとDummyDataFactory/bin/main/../../hoge1を返却 hoge1の直下に各テーブルのSQLがあるとの利用方法を想定しています。 DirectoryUtil.getPath("hoge2/fuga") だとDummyDataFactory/bin/main/../../../hoge1/fugaを返却 する処理となります。 DirectoryUtil DirectoryUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DirectoryUtil { private static final Logger logger = LoggerFactory.getLogger(DirectoryUtil.class); /** * dirNameに階層を指定する場合は/を指定してください。 * * @param dirName * @return 存在するdirNameのプロジェクト/bin/main/からの相対パス */ public static String getPath(String dirName) { logger.debug("getPath dirName={} start", dirName); URL url = DirectoryUtil.class.getClassLoader().getResource("."); if (url == null) { return null; } StringBuilder upperPathPrefix = new StringBuilder(".." + File.separator); var rootPath = url.getPath(); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if(SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } String currentTargetPath = null; int counter = 0; while (true) { currentTargetPath = rootPath + upperPathPrefix + dirName.replace("/", File.separator); if (Files.exists(Path.of(currentTargetPath))) { break; } if (counter > 5) { logger.error("getPath fail"); return null; } upperPathPrefix.append(".." + File.separator); counter++; } logger.debug("getPath result={}", currentTargetPath); return currentTargetPath; } } DirectoryUtilのテスト 期待値のディレクトリの作成後にテストを実施しております。ゴミディレクトリが残るのが・・・ DirectoryUtilTest package jp.small_java_world.dummydatafactory.util; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class DirectoryUtilTest { @ParameterizedTest @ValueSource(strings = { "hoge1", "hoge2/fuga" }) void testGetPath(String dirNameParam) throws IOException { URL url = DirectoryUtil.class.getClassLoader().getResource("."); var rootPath = url.getPath(); var dirName = dirNameParam.replace("/", File.separator); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if (SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } //DirectoryUtil.getPathがパスを検出するパスを作成 var targetDir = rootPath + ".." + File.separator + ".." + File.separator + dirName; //targetDirが存在しない場合は作成 if(!Files.exists(Path.of(targetDir))) { //targetDirに対応するディレクトリを作成 Files.createDirectories(Path.of(targetDir)); } var result = DirectoryUtil.getPath(dirName); //rootPathはテスト実行環境に依存するので、resultのrootPathを$rootPathに置換 result = result.replace(rootPath, "$rootPath"); //期待値も$rootPathからの相対パスで宣言 var expectedResult = "$rootPath" + ".." + File.separator + ".." + File.separator + dirName; assertEquals(expectedResult, result); } } SystemUtilはよくある実装だと思いますので、不要かもしれないですが、以下のようになります。 SystemUtil package jp.small_java_world.dummydatafactory.util; public class SystemUtil { public static boolean isWindows() { return System.getProperty("os.name").toLowerCase().startsWith("windows"); } } SQLを解析する処理 SqlAnalyzer Create TableのSQLの内容のStringが引数で、戻り値がMapとなります。 戻り値のMapのキー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnDataとなります。 それにしても、カラムの長さだけしか利用しないのに、処理がオーバースペックすぎますね・・・、まあ他の用途での利用も想定されますので・・・ SqlAnalyzer package jp.small_java_world.dummydatafactory.util; import java.util.HashMap; import java.util.Map; import jp.small_java_world.dummydatafactory.config.ColumnTypeConfig; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.create.table.CreateTable; public class SqlAnalyzer { public static Map<String, SqlColumnData> getSqlColumnDataMap(String sqlContent) throws JSQLParserException { //キー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnData Map<String, SqlColumnData> result = new HashMap<>(); //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); sqlColumnData.setDbDataType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setColumnName(columnDefinition.getColumnName()); //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } result.put(sqlColumnData.getColumnCamelCaseName(), sqlColumnData); } return result; } } SqlAnalyzer //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); JSqlParserを利用してCreate TableのSQLを解析しています。 createTable.getColumnDefinitions()で各カラムの解析結果を含んだListが取得できるので、SqlColumnDataのインスタンスを作成し、各カラムの結果を格納します。 SqlAnalyzer //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); SqlColumnData package jp.small_java_world.dummydatafactory.data; public class SqlColumnData { String columnName; String columnCamelCaseName; String javaType; String dbDataType; Integer dbDataSize; public String getColumnName() { return columnName; } public void setColumnName(String columnName) { this.columnName = columnName; } public String getColumnCamelCaseName() { return columnCamelCaseName; } public void setColumnCamelCaseName(String columnCamelCaseName) { this.columnCamelCaseName = columnCamelCaseName; } public String getJavaType() { return javaType; } public void setJavaType(String javaType) { this.javaType = javaType; } public String getDbDataType() { return dbDataType; } public void setDbDataType(String dbDataType) { this.dbDataType = dbDataType; } public Integer getDbDataSize() { return dbDataSize; } public void setDbDataSize(Integer dbDataSize) { this.dbDataSize = dbDataSize; } } columnDefinition.getColDataType().getDataType()でカラムのデータタイプ(character varyingやdateなど)が取得できますので、これをJavaの型に変換し、sqlColumnDataのjavaTypeに格納しています。 SqlAnalyzer //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); ColumnTypeConfig.getJavaType("character varying")だと"String"が返却されます。 ColumnTypeConfig.getJavaType("bigint")だと"Long"が返却されます。 columnType.ymlの内容は以下のようになっています。いろいろ足りてないですが・・・ columnType.yml character varying: String character: String integer: Integer timestamp: Timestamp timestamp with time zone: Timestamp smallint: Short date: Date bigint: Long ColumnTypeConfigですが、static初期化ブロックでcolumnType.ymlを読み込んでCOLUMN_TYPE_MAPを初期化し、 ColumnTypeConfig#getJavaTypeで指定された文字列をキーとするCOLUMN_TYPE_MAPのエントリーの値を返却しております。 ColumnTypeConfig package jp.small_java_world.dummydatafactory.config; import java.io.InputStream; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; public class ColumnTypeConfig { private static final Logger logger = LoggerFactory.getLogger(ColumnTypeConfig.class); private static Map<?, ?> COLUMN_TYPE_MAP; static { InputStream inputStream = ColumnTypeConfig.class.getResourceAsStream("/columnType.yml"); Yaml yaml = new Yaml(); COLUMN_TYPE_MAP = yaml.loadAs(inputStream, Map.class); } public static String getJavaType(String dbDataType) { if (COLUMN_TYPE_MAP.containsKey(dbDataType)) { logger.debug("dbDataType={} javaType={}", dbDataType, COLUMN_TYPE_MAP.get(dbDataType)); return COLUMN_TYPE_MAP.get(dbDataType).toString(); } else { logger.error("dbDataType={} javaType is not defined", dbDataType); } return null; } } カラム名に対応するJavaのクラスのメンバ名を生成し、sqlColumnDataのcolumnCamelCaseNameにセットしています。 SqlAnalyzer //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); StringConvertUtil package jp.small_java_world.dummydatafactory.util; import org.apache.commons.lang3.StringUtils; public class StringConvertUtil { public static String toSnakeCaseCase(String snakeCase) { StringBuilder stringBuilder = new StringBuilder(snakeCase.length() + 10); if (StringUtils.isEmpty(snakeCase)) { return snakeCase; } String firstUpperCase = firstLowerCase(snakeCase); String[] terms = StringUtils.splitPreserveAllTokens(firstUpperCase, "_", -1); stringBuilder.append(terms[0]); for (int i = 1, len = terms.length; i < len; i++) { stringBuilder.append(firstUpperCase(terms[i])); } return stringBuilder.toString(); } public static String toCamelCase(String camelCase) { if (StringUtils.isEmpty(camelCase)) { return camelCase; } StringBuilder stringBuilder = new StringBuilder(camelCase.length() + 10); String firstLowerCase = firstLowerCase(camelCase); char[] buf = firstLowerCase.toCharArray(); stringBuilder.append(buf[0]); for (int i = 1; i < buf.length; i++) { if ('A' <= buf[i] && buf[i] <= 'Z') { stringBuilder.append('_'); stringBuilder.append((char) (buf[i] + 0x20)); } else { stringBuilder.append(buf[i]); } } return stringBuilder.toString(); } private static String firstLowerCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toLowerCase().concat(target.substring(1)); } } public static String firstUpperCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toUpperCase().concat(target.substring(1)); } } } columnDefinition.getColDataType().getArgumentsStringList().get(0)にカラムサイズが格納されているので、sqlColumnDataのdbDataSizeに格納しています。 SqlAnalyzer //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } これで、JavaのクラスのString型のメンバの最大長を決定できます。 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 DummyDataFactory#generateDummyInstanceを以下のように変更します。 isEntity=trueで呼び出すと、対応するSQLを読み込み、対象クラスのインスタンスを生成します。 DummyDataFactory public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass, boolean isEntity) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } 変更点としては、 DummyDataFactory // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } と、RandomValueGenerator.generateRandomValueへ、SqlColumnDataの引数追加 DummyDataFactory // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); となります。 SqlFileUtilは以下のようになります。 SqlFileUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jp.small_java_world.dummydatafactory.config.CommonConfig; public class SqlFileUtil { private static final Logger logger = LoggerFactory.getLogger(SqlFileUtil.class); public static String getSqlContent(String targetClassSimpleName) throws IOException { //dummyDataFactorySetting.propertiesのsqlDirNameの値のディレクトリのパスを取得 var sqlFileDir = DirectoryUtil.getPath(CommonConfig.getSqlDirName()); var tableName = StringConvertUtil.toCamelCase(targetClassSimpleName).toLowerCase(); //dummyDataFactorySetting.propertiesのsqlFilePattern=create_$tableName.sql //からtargetClassSimpleNameに対応するsqlのファイル名を作成 var createSqlFileName = CommonConfig.getSqlFilePattern().replace("$tableName", tableName); var createSqlFilePath = Path.of(sqlFileDir + File.separator + createSqlFileName); //dummyDataFactorySetting.propertiesのsqlEndKeywordの値を取得 var sqlEndKeyword = CommonConfig.getSqlEndKeyword(); if (Files.exists(createSqlFilePath)) { var sqlContent = Files.readString(createSqlFilePath); //create indexなどを同じファイルに記載している場合解析に失敗するので、sqlEndKeywordの値以降は切り捨て if (StringUtils.isNotEmpty(sqlEndKeyword) && sqlContent.contains(sqlEndKeyword)) { sqlContent = sqlContent.substring(0, sqlContent.indexOf(sqlEndKeyword)); } logger.debug("getSqlContent return value {}", sqlContent); return sqlContent; } else { logger.error("not exist createSqlFile={}", createSqlFilePath); return null; } } } RandomValueGenerator#generateRandomValueを以下のように変更します。 RandomValueGenerator public static Object generateRandomValue(Class<?> type, SqlColumnData sqlColumnData) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } RandomValueGenerator#generateRandomValueの変更点 - .generateRandomString(DAFAULT_DATA_SIZE); + .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); DummyDataFactory#generateDummyInstanceのテスト DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.entity.FugaEntity; import jp.small_java_world.dummydatafactory.entity.HogeEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; import jp.small_java_world.dummydatafactory.util.SqlAnalyzer; import jp.small_java_world.dummydatafactory.util.SqlFileUtil; class DummyDataFactoryTest { @ParameterizedTest @ValueSource(strings = { "true", "false" }) void testGenerateDummyInstance(boolean isEntity) throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class); var sqlFileUtilMock = Mockito.mockStatic(SqlFileUtil.class); var sqlAnalyzerMock = Mockito.mockStatic(SqlAnalyzer.class)) { SqlColumnData integerMemberSqlColumnData = isEntity ? new SqlColumnData() : null; SqlColumnData shortMemberSqlColumnData = isEntity ? new SqlColumnData() : null; if (isEntity) { // SqlFileUtil.getSqlContent("dummyEntity")の振る舞いを定義 sqlFileUtilMock.when(() -> { SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()); }).thenReturn("dummy create sql"); // SqlAnalyzer.getSqlColumnDataMap("dummy create sql")の振る舞いを定義 sqlAnalyzerMock.when(() -> { SqlAnalyzer.getSqlColumnDataMap("dummy create sql"); }).thenReturn( Map.of("integerMember", integerMemberSqlColumnData, "shortMember", shortMemberSqlColumnData)); } // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType(), shortMemberSqlColumnData); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify // SqlFileUtil.getSqlContentとSqlAnalyzer.getSqlColumnDataMapはisEntity=trueのときのみ呼び出される。 sqlFileUtilMock.verify(() -> SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()), times(isEntity ? 1 : 0)); sqlAnalyzerMock.verify(() -> SqlAnalyzer.getSqlColumnDataMap("dummy create sql"), times(isEntity ? 1 : 0)); randomValueGeneratorMock.verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData), times(1)); randomValueGeneratorMock.verify(() -> RandomValueGenerator .generateRandomValue(shortMemberMemberField.getType(), shortMemberSqlColumnData), times(1)); } } } SqlAnalyzerのテスト SqlAnalyzerTest package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.JSQLParserException; class SqlAnalyzerTest { @Test void testGetSqlColumnDataMap() throws JSQLParserException { var result = SqlAnalyzer.getSqlColumnDataMap("CREATE TABLE todo( " + " id integer not null," + " title character varying(32)," + " content character varying(100)," + "limit_time timestamp with time zone," + "regist_date date" + ");"); assertThat(result).hasSize(5); assertThat(result).containsKeys("id", "content", "title", "limitTime", "registDate"); String targetKey = "id"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Integer"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("integer"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "title"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(32); targetKey = "content"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(100); targetKey = "limitTime"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("limit_time"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Timestamp"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("timestamp with time zone"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "registDate"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("regist_date"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Date"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("date"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); } } 完成版の全ソースは GitHubリポジトリ に登録しております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaで「FactoryBot」のようなものを実装してみる。

はじめに Rubyのgemの「FactoryBot」ですが、超有名プロダクトであることは衆目の一致するところです。 旧名称は「FactoryGirl」でしたので、こちらの方がピンとくる方も多いと思います。 テスト実装するときに、モックが返却する結果は手作業で実装することがほとんどだと思います。この実装が面倒ですよね。簡単な回避方法としては、共通化して、使いまわすぐらいでしょうか。 このテストデータの作り方の違いによって、テストが成功したり、失敗したりすることがあります。そんな場合は、テストの実装方法やプロダクトコードに問題がある場合が大半なのですが、せっかく実装したテストがテストデータ依存って、こんな悲しいことはありません。 テストを実行するたびに、新しいテストデータが生成され、テストデータ依存にならないテストが実装できればなーー、となります。 そんなときは「FactoryBot」です! ってJavaなんですけど・・・ ・・・ モックの戻り値の生成であれば、文字列の長さはそれほど問題にはなりませんが、DBに前提データとしてセットする場合は、長さの上限も重要となります。 本投稿ではカバーできていないのですが、コードや区分なども考慮しないと、実際のプロダクトコードのテストでストレスなく利用することは難しいので、この部分に関しては、今後の課題と考えております。 完成版の全ソースは GitHubリポジトリ に登録しております。 目的 以下を実現することを目的とします。 ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成ロジックの実装 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 利用するライブラリ JSqlParser Create TableのSQLを解析し、character varyingのようなデータタイプのカラム長を取得する用途で利用します。 JUnit5とAssertJ テストでJUnit5を利用します。 具体的には、junit-jupiter-api、junit-jupiter-engine、junit-jupiter-paramsを利用します。 「SnakeYAML」 YAMLを読み込むために利用します。 「Apache Commons Text」と「Apache Commons Lang 3」 ランダムな値の生成で利用します。 具体的には、 org.apache.commons.text.RandomStringGenerator org.apache.commons.lang3.RandomUtils を利用します。 開発環境 JDK11、Gradle、IDEですが、JavaのGradleプロジェクトが利用可能なものであればOKです。 IntelliJとEclipseでテストが通ることを確認済みです。 build.gradleは以下のようになります。 plugins { id 'java' } group 'jp.small_java_world' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.7.0' implementation group: 'org.yaml', name: 'snakeyaml', version: '1.28' implementation group: 'com.github.jsqlparser', name: 'jsqlparser', version: '4.0' implementation 'org.slf4j:slf4j-api:1.7.30' implementation 'ch.qos.logback:logback-core:1.2.3' implementation 'ch.qos.logback:logback-classic:1.2.3' implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' testCompile group: 'org.assertj', name: 'assertj-core', version: '3.14.0' testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.9.0' testImplementation group: 'org.mockito', name: 'mockito-inline', version: '3.9.0' } test { useJUnitPlatform() } ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成には、ランダムな値を生成するロジックが必要ですので、まずはここからはじめたいと思います。 RandomDataUtil package jp.small_java_world.dummydatafactory.util; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.text.RandomStringGenerator; public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = RandomUtils.nextInt(); var randInt2 = RandomUtils.nextInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } } 重複チェックのための各種Set RandomDataUtil static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); 生成した値が重複していないかを確認するための各種Setとなります。 generateRandomString ランダムな長さ指定の文字列生成メソッド RandomDataUtil public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } generateRandomLetterOrDigitをstart = '0', end = 'z'で呼び出し、結果がrandomStringCheckMapに存在しない場合は、その値を結果として返却します。 RandomDataUtil private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } generateRandomLetterOrDigitですが、charのレンジ指定(ASCIIコード)で生成した文字列を数値とアルファベットだけにし、長さ指定の文字列を返却します。 start = '0', end = 'z'の場合 '0'はASCIIコードで48(10進数)、'z'は同じく122(10進数)ですので、 ASCIIコードが91である'['も含みますので、filteredBy(Character::isLetterOrDigit)を指定して数値とアルファベットのみにフィルターしています。 generateRandomStringのテスト RandomDataUtilTestは初登場ですので、packageから張り付けております。 中身は、testGenerateRandomStringの関連メソッドだけにしております。 package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; class RandomDataUtilTest { // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } private void assertContainChar(String target, char startChar, char endChar) { assertContainChar(target, startChar, endChar, null); } private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } } testGenerateRandomStringメソッドですが、ParameterizedTestのValueSource指定となっており、RandomDataUtil.generateRandomStringの呼び出し時に指定するlengthのバリエーションテストとなります。 @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } assertContainCharは、生成したランダムな文字列が期待する文字種のみで構成されていることを検証するメソッドとなります。 先ほども説明させていただきましたが、ASCIIコードのレンジでの判定だけだと、記号も含まれますので、レンジ内の除外コードのリストを第三引数で指定可能となります。 private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); generateRandomHexString ランダムな長さ指定の16進数の文字列生成メソッド '0', 'f'のレンジだと、アルファベットの大文字小文字が含まれるので、大文字だけにするために final char start = '0', end = 'F';としております。 先頭が0になってしまうと、lengthの指定が無意味になってしまいますので、先頭の文字と2文字目以降の文字列を別々に生成しています。 RandomDataUtil public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomHexStringのテスト RandomDataUtil.generateRandomHexString(length)の結果は、ランダムな16進文字列ですので、 全体が'0', 'F'のレンジで、EXCLUDE_LETTER_OR_DIGIT_LISTを含まない、 先頭が'1', 'F'のレンジでEXCLUDE_LETTER_OR_DIGIT_LISTを含まない との検証を行っています。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomHexString(int length) { var result1 = RandomDataUtil.generateRandomHexString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'F'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result1.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomHexString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result2.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomNumberString ランダムな長さ指定の10進数の文字列生成メソッド generateRandomHexStringとよく似ています。全体のレンジが'1', '9'になっている部分だけ異なります。 RandomDataUtil public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomNumberStringのテスト 全体のレンジが'0', '9'ですので、EXCLUDE_LETTER_OR_DIGIT_LISTは考慮不要となります。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomNumberString(int length) { var result1 = RandomDataUtil.generateRandomNumberString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'9'の間に含まれていることを検証 assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result1.substring(0, 1), '1', '9'); var result2 = RandomDataUtil.generateRandomNumberString(length); assertThat(result2).hasSize(length); assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result2.substring(0, 1), '1', '9'); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomDate ランダムな値のjava.util.Date生成メソッド 現在のシステム時刻±365日の範囲内のDateを生成します。 RandomDataUtil public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } generateRandomDateのテスト RandomDataUtil#generateRandomTimestamp()がまだ説明できていないのですが、generateRandomDateとgenerateRandomTimestamp両方のテストとなります。 ParameterizedTestのEnumSource指定で、generateRandomDateとgenerateRandomTimestampの両方を対象とするテストとして実装しております。 RandomDataUtilTest private enum RandomDateTestType { DATE, TIMESTAMP; } @ParameterizedTest @EnumSource(RandomDateTestType.class) void testGenerateRandomDate(RandomDateTestType testType) { // 期待値の上限=now+365dayに10秒を足したDate // Calendar.getInstance()をテストで呼び出すタイミングと // RandomDataUtil.generateRandomDate()で呼び出すタイミングに時差があるので // 時差でテストが期待した動作にならずテストが失敗するパターンを排除するために10秒の猶予を設けています。 var calendarMax = Calendar.getInstance(); calendarMax.add(Calendar.DATE, 365); calendarMax.add(Calendar.SECOND, 10); var maxDate = calendarMax.getTime(); // 期待値の下限=now+365dayに-10秒を足したDate var calendarMin = Calendar.getInstance(); calendarMin.add(Calendar.DATE, -365); calendarMin.add(Calendar.SECOND, -10); var minDate = calendarMin.getTime(); Date result1 = null; if (testType == RandomDateTestType.DATE) { result1 = RandomDataUtil.generateRandomDate(); assertFalse(result1 instanceof Timestamp); } else if (testType == RandomDateTestType.TIMESTAMP) { result1 = RandomDataUtil.generateRandomTimestamp(); assertTrue(result1 instanceof Timestamp); } // calendarMaxの方がresult1より未来 assertTrue(maxDate.compareTo(result1) > 0); // calendarMinの方がresult1より過去 assertTrue(result1.compareTo(minDate) > 0); Date result2 = null; if (testType == RandomDateTestType.DATE) { result2 = RandomDataUtil.generateRandomDate(); } else if (testType == RandomDateTestType.TIMESTAMP) { result2 = RandomDataUtil.generateRandomTimestamp(); } // calendarMaxの方がresult2より未来 assertTrue(maxDate.compareTo(result2) > 0); // calendarMinの方がresult2より過去 assertTrue(result2.compareTo(minDate) > 0); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomTimestamp ランダムな値のjava.sql.Timestamp生成メソッド RandomDataUtil#generateRandomDate()の結果をTimestampに変換しているだけとなります。 RandomDataUtil public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } booleanと数値型のランダムな値生成メソッド RandomDataUtil public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } 数値型のランダムな値生成メソッドのテスト テストの見た目が貧弱なので、3回呼び出し、varで結果を受けているのでビジュアル的な訴求力が乏しいので、instanceofで型の確認を行っています。 RandomDataUtilTest @Test void testGenerateRandomInt() { var result1 = RandomDataUtil.generateRandomInt(); var result2 = RandomDataUtil.generateRandomInt(); var result3 = RandomDataUtil.generateRandomInt(); assertTrue(result1 instanceof Integer); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); assertThat(result2).isNotEqualTo(result3); } @Test void testGenerateRandomLong() { var result1 = RandomDataUtil.generateRandomLong(); var result2 = RandomDataUtil.generateRandomLong(); var result3 = RandomDataUtil.generateRandomLong(); assertTrue(result1 instanceof Long); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); assertThat(result2).isNotEqualTo(result3); } @Test void testGenerateRandomShort() { var result1 = RandomDataUtil.generateRandomShort(); var result2 = RandomDataUtil.generateRandomShort(); var result3 = RandomDataUtil.generateRandomShort(); assertTrue(result1 instanceof Short); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); assertThat(result2).isNotEqualTo(result3); } RandomDataUtilのリファクタリング while (true)の無限ループは問題です。短い長さを指定した場合や、一気に大量のテストケースを流した場合には、文字通り無限ループにはまります。 また、文字列系の生成メソッド(generateRandomString、generateRandomHexString、generateRandomNumberString)がびしょびしょ(!DRY)ですので、少しきれいにしてみます。 RandomDataUtil public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); static final int MAX_RETRY = 10; public static String generateRandomString(final int length) { return generateRandomString(length, '0', 'z', (char) 0); } public static String generateRandomHexString(final int length) { return generateRandomString(length, '0', 'F', '1'); } public static String generateRandomNumberString(final int length) { return generateRandomString(length, '0', '9', '1'); } public static String generateRandomString(final int length, final char start, final char end, final char firstStart) { int retryCount = 0; String firstResult = "", result = ""; while (true) { // firstStartがasciiコードの0でなければ先頭の1文字目と2文字目以降の生成を別に行う。 if (firstStart != 0) { firstResult = generateRandomLetterOrDigit(firstStart, end, 1); result = length == 1 ? firstResult : firstResult + generateRandomLetterOrDigit(start, end, length - 1); } else { result = generateRandomLetterOrDigit(start, end, length); } if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { int retryCount = 0; while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } if (retryCount++ > MAX_RETRY) { return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { int retryCount = 0; while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Integer generateRandomInt() { int retryCount = 0; while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Long generateRandomLong() { int retryCount = 0; while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { int retryCount = 0; while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } } リファクタリングと無限ループ回避(ってこれやっちゃいけないパターンですが・・・)後でもRandomDataUtilTestが問題なく成功するはずです。 「ランダムな値を含むインスタンスの生成ロジックの実装」は、これで完成となります。 今回は対象外となっておりますが、少し応用すれば、ランダムなIPアドレス、MACアドレス、メールアドレスなども比較的簡単に生成できると思います。 ランダムな値を含むインスタンスの生成ロジックの実装 DummyDataFactory#generateDummyInstance ランダムな値を含むインスタンスの生成メソッドDummyDataFactory#generateDummyInstance(Class)の説明をさせていただきます。 DummyDataFactory package jp.small_java_world.dummydatafactory; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } まずは、 DummyDataFactory // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } で生成対象のクラスとその親クラスに存在するメンバの配列(Field[] fields)を準備しています。 DummyDataFactory // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); 次に、生成対象のクラスのインスタンスの作成を行います。 最後に、fieldsをループし、RandomValueGenerator.generateRandomValue(type)でダミー値を生成、インスタンス(entity)に セットとなります。finalなフィールドは変更できませんので、無視しています。 DummyDataFactory // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } DummyDataFactory#generateDummyInstanceのテスト RandomValueGenerator.generateRandomValue(type)の説明の前に、DummyDataFactory#generateDummyInstanceのテストの説明をさせていただきます。 DummyEntityクラスを対象とし、ダミーインスタンスが正しく生成されることを確認しています。 DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class DummyDataFactoryTest { @Test void testGenerateDummyInstance() throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberField.getType()); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberField.getType()), times(1)); } } DummyEntity package jp.small_java_world.dummydatafactory.entity; public class DummyEntity { int integerMember; short shortMember; public int getIntegerMember() { return integerMember; } public short getShortMember() { return shortMember; } public void setIntegerMember(int integerMember) { this.integerMember = integerMember; } public void setShortMember(short shortMember) { this.shortMember = shortMember; } } まずは、DummyEntityクラスのフィールドを取得しています。 DummyDataFactoryTest var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); ReflectUtilは、以下のようになります。 ReflectUtil package jp.small_java_world.dummydatafactory.util; import java.lang.reflect.Field; public class ReflectUtil { public static void setStaticFieldValue(Class<?> targetClass, String fieldName, Object value) throws Exception { Field targetField = getDeclaredField(targetClass, fieldName); targetField.setAccessible(true); targetField.set(null, value); } public static void setFieldValue(Object targetObject, String fieldName, Object setValue) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); targetField.set(targetObject, setValue); } public static Object getFieldValue(Object targetObject, String fieldName) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); return targetField.get(targetObject); } public static Field getDeclaredField(Class<?> originalTargetClass, String fieldName) throws NoSuchFieldException { Class<?> targetClass = originalTargetClass; Field targetField = null; while (targetClass != null) { try { targetField = targetClass.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { targetClass = targetClass.getSuperclass(); } } return targetField; } } RandomValueGeneratorをモック化するために DummyDataFactoryTest try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { でrandomValueGeneratorMockを生成しています。RandomValueGenerator.generateRandomValueはstaticメソッドですので、Mockito.mockStaticを利用する必要があります。 randomValueGeneratorMockの振る舞いを定義します。 DummyDataFactoryTest // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberField.getType()); }).thenReturn((short) 101); これでDummyDataFactory.generateDummyInstance(DummyEntity.class)の結果が固定されますので DummyDataFactoryTest // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); と生成されたインスタンスの値を検証します。 最後に、randomValueGeneratorMockの振る舞いのverifyです。 DummyDataFactoryTest // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberField.getType()), times(1)); RandomValueGenerator#generateRandomValue RandomValueGenerator#generateRandomValue(type)は以下のようになります。 RandomValueGenerator package jp.small_java_world.dummydatafactory; import java.sql.Timestamp; import java.util.Date; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; public class RandomValueGenerator { public static final int DAFAULT_DATA_SIZE = 10; public static Object generateRandomValue(Class<?> type) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil.generateRandomString(DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } } if (type.isAssignableFrom(String.class)) { のようにtypeで分岐し、RandomDataUtilのtypeに対応するメソッドを呼び出し値を返却しています。 現時点では、Stringのサイズは10固定としています。 Stringはプリミティブ型が存在しないのでこれでOKなのですが、Integerのようにプリミティブ型が存在する場合は、以下のようにtype.getName().equals("int")のor条件が必要となります。 } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { RandomValueGenerator#generateRandomValueのテスト RandomValueGeneratorTargetDtoを対象クラスとするテストとなります。 RandomValueGeneratorTest package jp.small_java_world.dummydatafactory; import static org.junit.jupiter.api.Assertions.assertEquals; import java.sql.Timestamp; import java.util.Calendar; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.RandomValueGeneratorTargetDto; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class RandomValueGeneratorTest { private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberlong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { var targetField = ReflectUtil.getDeclaredField(RandomValueGeneratorTargetDto.class, testType.targetMemberName); try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomString(RandomValueGenerator.DAFAULT_DATA_SIZE); }).thenReturn(GenerateRandomValueTestType.STRING.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomLong(); }).thenReturn(testType == GenerateRandomValueTestType.LONG ? GenerateRandomValueTestType.LONG.mockResult : GenerateRandomValueTestType.PRIMITIVE_LONG.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomFloat(); }).thenReturn(testType == GenerateRandomValueTestType.FLOAT ? GenerateRandomValueTestType.FLOAT.mockResult : GenerateRandomValueTestType.PRIMITIVE_FLOAT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomShort(); }).thenReturn(testType == GenerateRandomValueTestType.SHORT ? GenerateRandomValueTestType.SHORT.mockResult : GenerateRandomValueTestType.PRIMITIVE_SHORT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomBool(); }).thenReturn( testType == GenerateRandomValueTestType.BOOLEAN ? GenerateRandomValueTestType.BOOLEAN.mockResult : GenerateRandomValueTestType.PRIMITIVE_BOOLEAN.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomDate(); }).thenReturn(GenerateRandomValueTestType.DATE.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomTimestamp(); }).thenReturn(GenerateRandomValueTestType.TIMESTAMP.mockResult); var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); } } } RandomValueGeneratorTargetDto package jp.small_java_world.dummydatafactory.entity; import java.sql.Timestamp; import java.util.Date; public class RandomValueGeneratorTargetDto { String memberString; Integer memberInteger; int memberInt; Long memberLong; long memberlong; Float memberFloat; float memberfloat; Short memberShort; short membershort; Boolean memberBoolean; boolean memberboolean; Date memberDate; Timestamp memberTimestamp; public String getMemberString() { return memberString; } public void setMemberString(String memberString) { this.memberString = memberString; } public Integer getMemberInteger() { return memberInteger; } public void setMemberInteger(Integer memberInteger) { this.memberInteger = memberInteger; } public int getMemberInt() { return memberInt; } public void setMemberInt(int memberInt) { this.memberInt = memberInt; } public Long getMemberLong() { return memberLong; } public void setMemberLong(Long memberLong) { this.memberLong = memberLong; } public long getMemberlong() { return memberlong; } public void setMemberlong(long memberlong) { this.memberlong = memberlong; } public Float getMemberFloat() { return memberFloat; } public void setMemberFloat(Float memberFloat) { this.memberFloat = memberFloat; } public float getMemberfloat() { return memberfloat; } public void setMemberfloat(float memberfloat) { this.memberfloat = memberfloat; } public Short getMemberShort() { return memberShort; } public void setMemberShort(Short memberShort) { this.memberShort = memberShort; } public short getMembershort() { return membershort; } public void setMembershort(short membershort) { this.membershort = membershort; } public Boolean getMemberBoolean() { return memberBoolean; } public void setMemberBoolean(Boolean memberBoolean) { this.memberBoolean = memberBoolean; } public boolean isMemberboolean() { return memberboolean; } public void setMemberboolean(boolean memberboolean) { this.memberboolean = memberboolean; } public Date getMemberDate() { return memberDate; } public Timestamp getMemberTimestamp() { return memberTimestamp; } public void setMemberDate(Date memberDate) { this.memberDate = memberDate; } public void setMemberTimestamp(Timestamp memberTimestamp) { this.memberTimestamp = memberTimestamp; } } テストメソッドtestGenerateRandomValueは、ParameterizedTestのEnumSource指定となります。 RandomValueGeneratorTest @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { GenerateRandomValueTestTypeは、RandomValueGeneratorTargetDtoの各メンバの名前と RandomValueGenerator.generateRandomValue(type:各メンバに対応する型); が呼びされた時の期待値を管理するenumとなります。 RandomValueGeneratorTest private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberlong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } RandomValueGeneratorTest try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { でRandomDataUtilをモック化し、GenerateRandomValueTestType testTypeのtargetMemberNameとmockResultでモックの振る舞いを定義し、 RandomValueGeneratorTest var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); でRandomValueGenerator.generateRandomValue(targetField.getType())の結果がtestTypeのmockResultと一致することを検証しています。 本テストでは、モックのverifyは行っていませんので、現在のテスト対象のEnum以外のモックの振る舞いも定義してますが RandomValueGeneratorTest randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); のようにRandomDataUtilの同一メソッドの振る舞いの分岐を行う必要があります。 これで「ランダムな値を含むインスタンスの生成ロジックの実装」は完了となります。 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 Create TableのSQLを解析し、JavaのString型のメンバの長さを決定する必要があります。 SQLのパスを特定 SQLを解析し、長さを特定 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 を実現していきます。 ロジックの実装に必要な処理の実装 Javaのプロジェクト/bin/main/パスから上位に上がって指定ディレクトリを探す処理 Create TableのSQLはバージョン管理システムで管理しており、ローカルのチェックアウト先のファイルをそのまま利用することを想定しております。テスト用のパスにコピーして利用では変更に弱いですので。 プロジェクト名がDummyDataFactoryの場合で、以下のようなディレクトリ構造の場合 DummyDataFactory/bin/main/  ├ ../../hoge1  ├ ../../../hoge2/fuga DirectoryUtil.getPath("hoge1") だとDummyDataFactory/bin/main/../../hoge1を返却 hoge1の直下に各テーブルのSQLがあるとの利用方法を想定しています。 DirectoryUtil.getPath("hoge2/fuga") だとDummyDataFactory/bin/main/../../../hoge1/fugaを返却 する処理となります。 DirectoryUtil DirectoryUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DirectoryUtil { private static final Logger logger = LoggerFactory.getLogger(DirectoryUtil.class); /** * dirNameに階層を指定する場合は/を指定してください。 * * @param dirName * @return 存在するdirNameのプロジェクト/bin/main/からの相対パス */ public static String getPath(String dirName) { logger.debug("getPath dirName={} start", dirName); URL url = DirectoryUtil.class.getClassLoader().getResource("."); if (url == null) { return null; } StringBuilder upperPathPrefix = new StringBuilder(".." + File.separator); var rootPath = url.getPath(); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if(SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } String currentTargetPath = null; int counter = 0; while (true) { currentTargetPath = rootPath + upperPathPrefix + dirName.replace("/", File.separator); if (Files.exists(Path.of(currentTargetPath))) { break; } if (counter > 5) { logger.error("getPath fail"); return null; } upperPathPrefix.append(".." + File.separator); counter++; } logger.debug("getPath result={}", currentTargetPath); return currentTargetPath; } } DirectoryUtilのテスト 期待値のディレクトリの作成後にテストを実施しております。ゴミディレクトリが残るのが・・・ DirectoryUtilTest package jp.small_java_world.dummydatafactory.util; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class DirectoryUtilTest { @ParameterizedTest @ValueSource(strings = { "hoge1", "hoge2/fuga" }) void testGetPath(String dirNameParam) throws IOException { URL url = DirectoryUtil.class.getClassLoader().getResource("."); var rootPath = url.getPath(); var dirName = dirNameParam.replace("/", File.separator); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if (SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } //DirectoryUtil.getPathがパスを検出するパスを作成 var targetDir = rootPath + ".." + File.separator + ".." + File.separator + dirName; //targetDirが存在しない場合は作成 if(!Files.exists(Path.of(targetDir))) { //targetDirに対応するディレクトリを作成 Files.createDirectories(Path.of(targetDir)); } var result = DirectoryUtil.getPath(dirName); //rootPathはテスト実行環境に依存するので、resultのrootPathを$rootPathに置換 result = result.replace(rootPath, "$rootPath"); //期待値も$rootPathからの相対パスで宣言 var expectedResult = "$rootPath" + ".." + File.separator + ".." + File.separator + dirName; assertEquals(expectedResult, result); } } SystemUtilはよくある実装だと思いますので、不要かもしれないですが、以下のようになります。 SystemUtil package jp.small_java_world.dummydatafactory.util; public class SystemUtil { public static boolean isWindows() { return System.getProperty("os.name").toLowerCase().startsWith("windows"); } } SQLを解析する処理 SqlAnalyzer Create TableのSQLの内容のStringが引数で、戻り値がMapとなります。 戻り値のMapのキー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnDataとなります。 それにしても、カラムの長さだけしか利用しないのに、処理がオーバースペックすぎますね・・・、まあ他の用途での利用も想定されますので・・・ SqlAnalyzer package jp.small_java_world.dummydatafactory.util; import java.util.HashMap; import java.util.Map; import jp.small_java_world.dummydatafactory.config.ColumnTypeConfig; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.create.table.CreateTable; public class SqlAnalyzer { public static Map<String, SqlColumnData> getSqlColumnDataMap(String sqlContent) throws JSQLParserException { //キー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnData Map<String, SqlColumnData> result = new HashMap<>(); //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); sqlColumnData.setDbDataType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setColumnName(columnDefinition.getColumnName()); //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } result.put(sqlColumnData.getColumnCamelCaseName(), sqlColumnData); } return result; } } SqlAnalyzer //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); JSqlParserを利用してCreate TableのSQLを解析しています。 createTable.getColumnDefinitions()で各カラムの解析結果を含んだListが取得できるので、SqlColumnDataのインスタンスを作成し、各カラムの結果を格納します。 SqlAnalyzer //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); SqlColumnData package jp.small_java_world.dummydatafactory.data; public class SqlColumnData { String columnName; String columnCamelCaseName; String javaType; String dbDataType; Integer dbDataSize; public String getColumnName() { return columnName; } public void setColumnName(String columnName) { this.columnName = columnName; } public String getColumnCamelCaseName() { return columnCamelCaseName; } public void setColumnCamelCaseName(String columnCamelCaseName) { this.columnCamelCaseName = columnCamelCaseName; } public String getJavaType() { return javaType; } public void setJavaType(String javaType) { this.javaType = javaType; } public String getDbDataType() { return dbDataType; } public void setDbDataType(String dbDataType) { this.dbDataType = dbDataType; } public Integer getDbDataSize() { return dbDataSize; } public void setDbDataSize(Integer dbDataSize) { this.dbDataSize = dbDataSize; } } columnDefinition.getColDataType().getDataType()でカラムのデータタイプ(character varyingやdateなど)が取得できますので、これをJavaの型に変換し、sqlColumnDataのjavaTypeに格納しています。 SqlAnalyzer //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); ColumnTypeConfig.getJavaType("character varying")だと"String"が返却されます。 ColumnTypeConfig.getJavaType("bigint")だと"Long"が返却されます。 columnType.ymlの内容は以下のようになっています。いろいろ足りてないですが・・・ columnType.yml character varying: String character: String integer: Integer timestamp: Timestamp timestamp with time zone: Timestamp smallint: Short date: Date bigint: Long ColumnTypeConfigですが、static初期化ブロックでcolumnType.ymlを読み込んでCOLUMN_TYPE_MAPを初期化し、 ColumnTypeConfig#getJavaTypeで指定された文字列をキーとするCOLUMN_TYPE_MAPのエントリーの値を返却しております。 ColumnTypeConfig package jp.small_java_world.dummydatafactory.config; import java.io.InputStream; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; public class ColumnTypeConfig { private static final Logger logger = LoggerFactory.getLogger(ColumnTypeConfig.class); private static Map<?, ?> COLUMN_TYPE_MAP; static { InputStream inputStream = ColumnTypeConfig.class.getResourceAsStream("/columnType.yml"); Yaml yaml = new Yaml(); COLUMN_TYPE_MAP = yaml.loadAs(inputStream, Map.class); } public static String getJavaType(String dbDataType) { if (COLUMN_TYPE_MAP.containsKey(dbDataType)) { logger.debug("dbDataType={} javaType={}", dbDataType, COLUMN_TYPE_MAP.get(dbDataType)); return COLUMN_TYPE_MAP.get(dbDataType).toString(); } else { logger.error("dbDataType={} javaType is not defined", dbDataType); } return null; } } カラム名に対応するJavaのクラスのメンバ名を生成し、sqlColumnDataのcolumnCamelCaseNameにセットしています。 SqlAnalyzer //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); StringConvertUtil package jp.small_java_world.dummydatafactory.util; import org.apache.commons.lang3.StringUtils; public class StringConvertUtil { public static String toSnakeCaseCase(String snakeCase) { StringBuilder stringBuilder = new StringBuilder(snakeCase.length() + 10); if (StringUtils.isEmpty(snakeCase)) { return snakeCase; } String firstUpperCase = firstLowerCase(snakeCase); String[] terms = StringUtils.splitPreserveAllTokens(firstUpperCase, "_", -1); stringBuilder.append(terms[0]); for (int i = 1, len = terms.length; i < len; i++) { stringBuilder.append(firstUpperCase(terms[i])); } return stringBuilder.toString(); } public static String toCamelCase(String camelCase) { if (StringUtils.isEmpty(camelCase)) { return camelCase; } StringBuilder stringBuilder = new StringBuilder(camelCase.length() + 10); String firstLowerCase = firstLowerCase(camelCase); char[] buf = firstLowerCase.toCharArray(); stringBuilder.append(buf[0]); for (int i = 1; i < buf.length; i++) { if ('A' <= buf[i] && buf[i] <= 'Z') { stringBuilder.append('_'); stringBuilder.append((char) (buf[i] + 0x20)); } else { stringBuilder.append(buf[i]); } } return stringBuilder.toString(); } private static String firstLowerCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toLowerCase().concat(target.substring(1)); } } public static String firstUpperCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toUpperCase().concat(target.substring(1)); } } } columnDefinition.getColDataType().getArgumentsStringList().get(0)にカラムサイズが格納されているので、sqlColumnDataのdbDataSizeに格納しています。 SqlAnalyzer //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } これで、JavaのクラスのString型のメンバの最大長を決定できます。 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 DummyDataFactory#generateDummyInstanceを以下のように変更します。 isEntity=trueで呼び出すと、対応するSQLを読み込み、対象クラスのインスタンスを生成します。 DummyDataFactory public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass, boolean isEntity) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } 変更点としては、 DummyDataFactory // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } と、RandomValueGenerator.generateRandomValueへ、SqlColumnDataの引数追加 DummyDataFactory // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); となります。 SqlFileUtilは以下のようになります。 SqlFileUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jp.small_java_world.dummydatafactory.config.CommonConfig; public class SqlFileUtil { private static final Logger logger = LoggerFactory.getLogger(SqlFileUtil.class); public static String getSqlContent(String targetClassSimpleName) throws IOException { //dummyDataFactorySetting.propertiesのsqlDirNameの値のディレクトリのパスを取得 var sqlFileDir = DirectoryUtil.getPath(CommonConfig.getSqlDirName()); var tableName = StringConvertUtil.toCamelCase(targetClassSimpleName).toLowerCase(); //dummyDataFactorySetting.propertiesのsqlFilePattern=create_$tableName.sql //からtargetClassSimpleNameに対応するsqlのファイル名を作成 var createSqlFileName = CommonConfig.getSqlFilePattern().replace("$tableName", tableName); var createSqlFilePath = Path.of(sqlFileDir + File.separator + createSqlFileName); //dummyDataFactorySetting.propertiesのsqlEndKeywordの値を取得 var sqlEndKeyword = CommonConfig.getSqlEndKeyword(); if (Files.exists(createSqlFilePath)) { var sqlContent = Files.readString(createSqlFilePath); //create indexなどを同じファイルに記載している場合解析に失敗するので、sqlEndKeywordの値以降は切り捨て if (StringUtils.isNotEmpty(sqlEndKeyword) && sqlContent.contains(sqlEndKeyword)) { sqlContent = sqlContent.substring(0, sqlContent.indexOf(sqlEndKeyword)); } logger.debug("getSqlContent return value {}", sqlContent); return sqlContent; } else { logger.error("not exist createSqlFile={}", createSqlFilePath); return null; } } } RandomValueGenerator#generateRandomValueを以下のように変更します。 RandomValueGenerator public static Object generateRandomValue(Class<?> type, SqlColumnData sqlColumnData) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } RandomValueGenerator#generateRandomValueの変更点 - .generateRandomString(DAFAULT_DATA_SIZE); + .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); DummyDataFactory#generateDummyInstanceのテスト DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.entity.FugaEntity; import jp.small_java_world.dummydatafactory.entity.HogeEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; import jp.small_java_world.dummydatafactory.util.SqlAnalyzer; import jp.small_java_world.dummydatafactory.util.SqlFileUtil; class DummyDataFactoryTest { @ParameterizedTest @ValueSource(strings = { "true", "false" }) void testGenerateDummyInstance(boolean isEntity) throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class); var sqlFileUtilMock = Mockito.mockStatic(SqlFileUtil.class); var sqlAnalyzerMock = Mockito.mockStatic(SqlAnalyzer.class)) { SqlColumnData integerMemberSqlColumnData = isEntity ? new SqlColumnData() : null; SqlColumnData shortMemberSqlColumnData = isEntity ? new SqlColumnData() : null; if (isEntity) { // SqlFileUtil.getSqlContent("dummyEntity")の振る舞いを定義 sqlFileUtilMock.when(() -> { SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()); }).thenReturn("dummy create sql"); // SqlAnalyzer.getSqlColumnDataMap("dummy create sql")の振る舞いを定義 sqlAnalyzerMock.when(() -> { SqlAnalyzer.getSqlColumnDataMap("dummy create sql"); }).thenReturn( Map.of("integerMember", integerMemberSqlColumnData, "shortMember", shortMemberSqlColumnData)); } // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberField.getType(), shortMemberSqlColumnData); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify // SqlFileUtil.getSqlContentとSqlAnalyzer.getSqlColumnDataMapはisEntity=trueのときのみ呼び出される。 sqlFileUtilMock.verify(() -> SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()), times(isEntity ? 1 : 0)); sqlAnalyzerMock.verify(() -> SqlAnalyzer.getSqlColumnDataMap("dummy create sql"), times(isEntity ? 1 : 0)); randomValueGeneratorMock.verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData), times(1)); randomValueGeneratorMock.verify(() -> RandomValueGenerator .generateRandomValue(shortMemberField.getType(), shortMemberSqlColumnData), times(1)); } } } SqlAnalyzerのテスト SqlAnalyzerTest package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.JSQLParserException; class SqlAnalyzerTest { @Test void testGetSqlColumnDataMap() throws JSQLParserException { var result = SqlAnalyzer.getSqlColumnDataMap("CREATE TABLE todo( " + " id integer not null," + " title character varying(32)," + " content character varying(100)," + "limit_time timestamp with time zone," + "regist_date date" + ");"); assertThat(result).hasSize(5); assertThat(result).containsKeys("id", "content", "title", "limitTime", "registDate"); String targetKey = "id"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Integer"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("integer"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "title"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(32); targetKey = "content"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(100); targetKey = "limitTime"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("limit_time"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Timestamp"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("timestamp with time zone"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "registDate"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("regist_date"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Date"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("date"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); } } 完成版の全ソースは GitHubリポジトリ に登録しております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaで「Factory Bot」のようものを実装してみる。

はじめに Rubyのgemの「Factory Bot」ですが、超有名プロダクトであることは衆目の一致するところです。 旧名称は「Factory Girl」でしたので、こちらの方がピンとくる方も多いと思います。 テスト実装するときに、モックが返却する結果は手作業で実装することがほとんどだと思います。この実装が面倒ですよね。簡単な回避方法としては、共通化して、使いまわすぐらいでしょうか。 このテストデータの作り方の違いによって、テストが成功したり、失敗したりすることがあります。そんな場合は、テストの実装方法やプロダクトコードに問題がある場合が大半なのですが、せっかく実装したテストがテストデータ依存って、こんな悲しいことはありません。 テストを実行するたびに、新しいテストデータが生成され、テストデータ依存にならないテストが実装できればなーー、となります。 そんなときは「Factory Bot」です! ってJavaなんですけど・・・ ・・・ モックの戻り値の生成であれば、文字列の長さはそれほど問題にはなりませんが、DBに前提データとしてセットする場合は、長さの上限も重要となります。 完成版の全ソースは GitHubリポジトリ に登録しております。 目的 以下を実現することを目的とします。 ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成ロジックの実装 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 利用するライブラリ JSqlParser Create TableのSQLを解析し、character varyingのようなデータタイプのカラム長を取得する用途で利用します。 JUnit5とAssertJ テストでJUnit5を利用します。 具体的には、junit-jupiter-api、junit-jupiter-engine、junit-jupiter-paramsを利用します。 「SnakeYAML」 YAMLを読み込むために利用します。 「Apache Commons Text」と「Apache Commons Lang 3」 ランダムな値の生成で利用します。 具体的には、 org.apache.commons.text.RandomStringGenerator org.apache.commons.lang3.RandomUtils を利用します。 開発環境 JDK11、Gradle、IDEですが、JavaのGradleプロジェクトが利用可能なものであればOKです。 IntelliJとEclipseでテストが通ることを確認済みです。 build.gradleは以下のようになります。 plugins { id 'java' } group 'jp.small_java_world' version '1.0-SNAPSHOT' repositories { mavenCentral() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.7.0' implementation group: 'org.yaml', name: 'snakeyaml', version: '1.28' implementation group: 'com.github.jsqlparser', name: 'jsqlparser', version: '4.0' implementation 'org.slf4j:slf4j-api:1.7.30' implementation 'ch.qos.logback:logback-core:1.2.3' implementation 'ch.qos.logback:logback-classic:1.2.3' implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' testCompile group: 'org.assertj', name: 'assertj-core', version: '3.14.0' testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.9.0' testImplementation group: 'org.mockito', name: 'mockito-inline', version: '3.9.0' } test { useJUnitPlatform() } ランダムな値の生成ロジックの実装 ランダムな値を含むインスタンスの生成には、ランダムな値を生成するロジックが必要ですので、まずはここからはじめたいと思います。 RandomDataUtil package jp.small_java_world.dummydatafactory.util; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.text.RandomStringGenerator; public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } } 重複チェックのための各種Set RandomDataUtil static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); 生成した値が重複していないかを確認するための各種Setとなります。 generateRandomString ランダムな長さ指定の文字列生成メソッド RandomDataUtil public static String generateRandomString(final int length) { final char start = '0', end = 'z'; while (true) { String result = generateRandomLetterOrDigit(start, end, length); if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } } } generateRandomLetterOrDigitをstart = '0', end = 'z'で呼び出し、結果がrandomStringCheckMapに存在しない場合は、その値を結果として返却します。 RandomDataUtil private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } generateRandomLetterOrDigitですが、charのレンジ指定(ASCIIコード)で生成した文字列を数値とアルファベットだけにし、長さ指定の文字列を返却します。 start = '0', end = 'z'の場合 '0'はASCIIコードで48(10進数)、'z'は同じく122(10進数)ですので、 ASCIIコードが91である'['も含みますので、filteredBy(Character::isLetterOrDigit)を指定して数値とアルファベットのみにフィルターしています。 generateRandomStringのテスト RandomDataUtilTestは初登場ですので、packageから張り付けております。 中身は、testGenerateRandomStringの関連メソッドだけにしております。 package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; class RandomDataUtilTest { // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } private void assertContainChar(String target, char startChar, char endChar) { assertContainChar(target, startChar, endChar, null); } private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } } testGenerateRandomStringメソッドですが、ParameterizedTestのValueSource指定となっており、RandomDataUtil.generateRandomStringの呼び出し時に指定するlengthのバリエーションテストとなります。 @ParameterizedTest @ValueSource(ints = { 1, 2, 10 }) void testGenerateRandomString(int length) { var result1 = RandomDataUtil.generateRandomString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'z'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'z', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } assertContainCharは、生成したランダムな文字列が期待する文字種のみで構成されていることを検証するメソッドとなります。 先ほども説明させていただきましたが、ASCIIコードのレンジでの判定だけだと、記号も含まれますので、レンジ内の除外コードのリストを第三引数で指定可能となります。 private void assertContainChar(String target, char startChar, char endChar, List<Integer> excludeList) { int start = (int) startChar; int end = (int) endChar; char[] charArray = target.toCharArray(); for (int i = 0; i < charArray.length; i++) { int current = (int) charArray[i]; if (current < start || end < current) { fail(String.format("%dは範囲(%d-%d)の文字コードに含まれない", current, start, end)); } else if (excludeList != null && excludeList.contains(current)) { fail(String.format("%dはexcludeListに含まれる文字コード", current)); } } } // [ \]^_`などはASCIIコードの0からz間でもLetterOrDigitでないので、これらが除外さていることを確認するときに利用 final List<Integer> EXCLUDE_LETTER_OR_DIGIT_LIST = List.of(58, 59, 60, 61, 62, 64, 91, 92, 93, 94, 95, 96); generateRandomHexString ランダムな長さ指定の16進数の文字列生成メソッド '0', 'f'のレンジだと、アルファベットの大文字小文字が含まれるので、大文字だけにするために final char start = '0', end = 'F';としております。 先頭が0になってしまうと、lengthの指定が無意味になってしまいますので、先頭の文字と2文字目以降の文字列を別々に生成しています。 RandomDataUtil public static String generateRandomHexString(final int length) { final char start = '0', end = 'F'; while (true) { String first = generateRandomLetterOrDigit('1', 'F', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomHexStringのテスト RandomDataUtil.generateRandomHexString(length)の結果は、ランダムな16進文字列ですので、 全体が'0', 'F'のレンジで、EXCLUDE_LETTER_OR_DIGIT_LISTを含まない、 先頭が'1', 'F'のレンジでEXCLUDE_LETTER_OR_DIGIT_LISTを含まない との検証を行っています。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomHexString(int length) { var result1 = RandomDataUtil.generateRandomHexString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'F'の間に含まれていて、EXCLUDE_LETTER_OR_DIGIT_LISTに含まれていないことを検証 assertContainChar(result1, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result1.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); var result2 = RandomDataUtil.generateRandomHexString(length); assertThat(result2).hasSize(length); assertContainChar(result2, '0', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // 先頭は0以外 assertContainChar(result2.substring(0, 1), '1', 'F', EXCLUDE_LETTER_OR_DIGIT_LIST); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomNumberString ランダムな長さ指定の10進数の文字列生成メソッド generateRandomHexStringとよく似ています。全体のレンジが'1', '9'になっている部分だけ異なります。 RandomDataUtil public static String generateRandomNumberString(final int length) { final char start = '0', end = '9'; while (true) { String first = generateRandomLetterOrDigit('1', '9', 1); String result = generateRandomLetterOrDigit(start, end, length - 1); if (!randomStringCheckMap.contains(first + result)) { randomStringCheckMap.add(first + result); return first + result; } } } generateRandomNumberStringのテスト 全体のレンジが'0', '9'ですので、EXCLUDE_LETTER_OR_DIGIT_LISTは考慮不要となります。 RandomDataUtilTest @ParameterizedTest @ValueSource(ints = { 1, 2, 20 }) void testGenerateRandomNumberString(int length) { var result1 = RandomDataUtil.generateRandomNumberString(length); assertThat(result1).hasSize(length); // result1が文字コード'0'と'9'の間に含まれていることを検証 assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result1.substring(0, 1), '1', '9'); var result2 = RandomDataUtil.generateRandomNumberString(length); assertThat(result2).hasSize(length); assertContainChar(result1, '0', '9'); // 先頭は0以外の数字 assertContainChar(result2.substring(0, 1), '1', '9'); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomDate ランダムな値のjava.util.Date生成メソッド 現在のシステム時刻±365日の範囲内のDateを生成します。 RandomDataUtil public static Date generateRandomDate() { while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } } } generateRandomDateのテスト RandomDataUtil#generateRandomTimestamp()がまだ説明できていないのですが、generateRandomDateとgenerateRandomTimestamp両方のテストとなります。 ParameterizedTestのEnumSource指定で、generateRandomDateとgenerateRandomTimestampの両方を対象とするテストとして実装しております。 RandomDataUtilTest private enum RandomDateTestType { DATE, TIMESTAMP; } @ParameterizedTest @EnumSource(RandomDateTestType.class) void testGenerateRandomDate(RandomDateTestType testType) { // 期待値の上限=now+365dayに10秒を足したDate // Calendar.getInstance()をテストで呼び出すタイミングと // RandomDataUtil.generateRandomDate()で呼び出すタイミングに時差があるので // 時差でテストが期待した動作にならずテストが失敗するパターンを排除するために10秒の猶予を設けています。 var calendarMax = Calendar.getInstance(); calendarMax.add(Calendar.DATE, 365); calendarMax.add(Calendar.SECOND, 10); var maxDate = calendarMax.getTime(); // 期待値の下限=now+365dayに-10秒を足したDate var calendarMin = Calendar.getInstance(); calendarMin.add(Calendar.DATE, -365); calendarMin.add(Calendar.SECOND, -10); var minDate = calendarMin.getTime(); Date result1 = null; if (testType == RandomDateTestType.DATE) { result1 = RandomDataUtil.generateRandomDate(); assertFalse(result1 instanceof Timestamp); } else if (testType == RandomDateTestType.TIMESTAMP) { result1 = RandomDataUtil.generateRandomTimestamp(); assertTrue(result1 instanceof Timestamp); } // calendarMaxの方がresult1より未来 assertTrue(maxDate.compareTo(result1) > 0); // calendarMinの方がresult1より過去 assertTrue(result1.compareTo(minDate) > 0); Date result2 = null; if (testType == RandomDateTestType.DATE) { result2 = RandomDataUtil.generateRandomDate(); } else if (testType == RandomDateTestType.TIMESTAMP) { result2 = RandomDataUtil.generateRandomTimestamp(); } // calendarMaxの方がresult2より未来 assertTrue(maxDate.compareTo(result2) > 0); // calendarMinの方がresult2より過去 assertTrue(result2.compareTo(minDate) > 0); // result1とresult2が一致しないこと assertThat(result2).isNotEqualTo(result1); } generateRandomTimestamp ランダムな値のjava.sql.Timestamp生成メソッド RandomDataUtil#generateRandomDate()の結果をTimestampに変換しているだけとなります。 RandomDataUtil public static Timestamp generateRandomTimestamp() { while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } } } booleanと数値型のランダムな値生成メソッド RandomDataUtil public static Integer generateRandomInt() { while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } } } public static Long generateRandomLong() { while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } } } 数値型のランダムな値生成メソッドのテスト テストの見た目が貧弱なので、3回呼び出し、varで結果を受けているのでビジュアル的な訴求力が乏しいので、instanceofで型の確認を行っています。 RandomDataUtilTest @Test void testGenerateRandomInt() { var result1 = RandomDataUtil.generateRandomInt(); var result2 = RandomDataUtil.generateRandomInt(); var result3 = RandomDataUtil.generateRandomInt(); assertTrue(result1 instanceof Integer); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } @Test void testGenerateRandomLong() { var result1 = RandomDataUtil.generateRandomLong(); var result2 = RandomDataUtil.generateRandomLong(); var result3 = RandomDataUtil.generateRandomLong(); assertTrue(result1 instanceof Long); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } @Test void testGenerateRandomShort() { var result1 = RandomDataUtil.generateRandomShort(); var result2 = RandomDataUtil.generateRandomShort(); var result3 = RandomDataUtil.generateRandomShort(); assertTrue(result1 instanceof Short); assertThat(result1).isNotEqualTo(result2); assertThat(result1).isNotEqualTo(result3); } RandomDataUtilのリファクタリング while (true)の無限ループは問題です。短い長さを指定した場合や、一気に大量のテストケースを流した場合には、文字通り無限ループにはまります。 また、文字列系の生成メソッド(generateRandomString、generateRandomHexString、generateRandomNumberString)がびしょびしょ(!DRY)ですので、少しきれいにしてみます。 RandomDataUtil public class RandomDataUtil { static Set<Date> randomDateCheckMap = new HashSet<Date>(); static Set<Timestamp> randomTimestampCheckMap = new HashSet<Timestamp>(); static Set<Long> randomLongCheckMap = new HashSet<Long>(); static Set<Integer> randomIntCheckMap = new HashSet<Integer>(); static Set<Short> randomShortCheckMap = new HashSet<Short>(); static Set<String> randomStringCheckMap = new HashSet<String>(); static final int MAX_RETRY = 10; public static String generateRandomString(final int length) { return generateRandomString(length, '0', 'z', (char) 0); } public static String generateRandomHexString(final int length) { return generateRandomString(length, '0', 'F', '1'); } public static String generateRandomNumberString(final int length) { return generateRandomString(length, '0', '9', '1'); } public static String generateRandomString(final int length, final char start, final char end, final char firstStart) { int retryCount = 0; String firstResult = "", result = ""; while (true) { // firstStartがasciiコードの0でなければ先頭の1文字目と2文字目以降の生成を別に行う。 if (firstStart != 0) { firstResult = generateRandomLetterOrDigit(firstStart, end, 1); result = length == 1 ? firstResult : firstResult + generateRandomLetterOrDigit(start, end, length - 1); } else { result = generateRandomLetterOrDigit(start, end, length); } if (!randomStringCheckMap.contains(result)) { randomStringCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } private static String generateRandomLetterOrDigit(char start, char end, final int length) { return new RandomStringGenerator.Builder().withinRange(start, end).filteredBy(Character::isLetterOrDigit) .build().generate(length); } public static Date generateRandomDate() { int retryCount = 0; while (true) { var calendar = Calendar.getInstance(); boolean isAdd = RandomUtils.nextBoolean(); calendar.add(Calendar.DATE, isAdd ? RandomUtils.nextInt(1, 365) : -1 * RandomUtils.nextInt(1, 365)); if (!randomDateCheckMap.contains(calendar.getTime())) { randomDateCheckMap.add(calendar.getTime()); return calendar.getTime(); } if (retryCount++ > MAX_RETRY) { return calendar.getTime(); } } } public static Timestamp generateRandomTimestamp() { int retryCount = 0; while (true) { var result = new Timestamp(generateRandomDate().getTime()); if (!randomTimestampCheckMap.contains(result)) { randomTimestampCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Integer generateRandomInt() { int retryCount = 0; while (true) { Integer result = RandomUtils.nextInt(); if (!randomIntCheckMap.contains(result)) { randomIntCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Long generateRandomLong() { int retryCount = 0; while (true) { Long result = RandomUtils.nextLong(); if (!randomLongCheckMap.contains(result)) { randomLongCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } public static Float generateRandomFloat() { var randInt1 = generateRandomInt(); var randInt2 = generateRandomInt(); return (float) (randInt1 / randInt2); } public static boolean generateRandomBool() { return RandomUtils.nextBoolean(); } public static Short generateRandomShort() { int retryCount = 0; while (true) { Short result = (short) RandomUtils.nextInt(0, Short.MAX_VALUE); if (!randomShortCheckMap.contains(result)) { randomShortCheckMap.add(result); return result; } if (retryCount++ > MAX_RETRY) { return result; } } } } リファクタリングと無限ループ回避(ってこれやっちゃいけないパターンですが・・・)後でもRandomDataUtilTestが問題なく成功するはずです。 「ランダムな値を含むインスタンスの生成ロジックの実装」は、これで完成となります。 ランダムな値を含むインスタンスの生成ロジックの実装 DummyDataFactory#generateDummyInstance ランダムな値を含むインスタンスの生成メソッドDummyDataFactory#generateDummyInstance(Class)の説明をさせていただきます。 DummyDataFactory package jp.small_java_world.dummydatafactory; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } まずは、 DummyDataFactory // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } で生成対象のクラスとその親クラスに存在するメンバの配列(Field[] fields)を準備しています。 DummyDataFactory // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); 次に、生成対象のクラスのインスタンスの作成を行います。 最後に、fieldsをループし、RandomValueGenerator.generateRandomValue(type)でダミー値を生成、インスタンス(entity)に セットとなります。finalなフィールドは変更できませんので、無視しています。 DummyDataFactory // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } DummyDataFactory#generateDummyInstanceのテスト RandomValueGenerator.generateRandomValue(type)の説明の前に、DummyDataFactory#generateDummyInstanceのテストの説明をさせていただきます。 DummyEntityクラスを対象とし、ダミーインスタンスが正しく生成されることを確認しています。 DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class DummyDataFactoryTest { @Test void testGenerateDummyInstance() throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()), times(1)); } } DummyEntity package jp.small_java_world.dummydatafactory.entity; public class DummyEntity { int integerMember; short shortMember; public int getIntegerMember() { return integerMember; } public short getShortMember() { return shortMember; } public void setIntegerMember(int integerMember) { this.integerMember = integerMember; } public void setShortMember(short shortMember) { this.shortMember = shortMember; } } まずは、DummyEntityクラスのフィールドを取得しています。 DummyDataFactoryTest var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); ReflectUtilは、以下のようになります。 ReflectUtil package jp.small_java_world.dummydatafactory.util; import java.lang.reflect.Field; public class ReflectUtil { public static void setStaticFieldValue(Class<?> targetClass, String fieldName, Object value) throws Exception { Field targetField = getDeclaredField(targetClass, fieldName); targetField.setAccessible(true); targetField.set(null, value); } public static void setFieldValue(Object targetObject, String fieldName, Object setValue) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); targetField.set(targetObject, setValue); } public static Object getFieldValue(Object targetObject, String fieldName) throws Exception { Field targetField = getDeclaredField(targetObject.getClass(), fieldName); targetField.setAccessible(true); return targetField.get(targetObject); } public static Field getDeclaredField(Class<?> originalTargetClass, String fieldName) throws NoSuchFieldException { Class<?> targetClass = originalTargetClass; Field targetField = null; while (targetClass != null) { try { targetField = targetClass.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { targetClass = targetClass.getSuperclass(); } } return targetField; } } RandomValueGeneratorをモック化するために DummyDataFactoryTest try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class)) { でrandomValueGeneratorMockを生成しています。RandomValueGenerator.generateRandomValueはstaticメソッドですので、Mockito.mockStaticを利用する必要があります。 randomValueGeneratorMockの振る舞いを定義します。 DummyDataFactoryTest // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType()); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()); }).thenReturn((short) 101); これでDummyDataFactory.generateDummyInstance(DummyEntity.class)の結果が固定されますので DummyDataFactoryTest // DummyDataFactory.generateDummyInstance(DummyEntity.class)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); と生成されたインスタンスの値を検証します。 最後に、randomValueGeneratorMockの振る舞いのverifyです。 DummyDataFactoryTest // モックのverify randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType()), times(1)); randomValueGeneratorMock .verify(() -> RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType()), times(1)); RandomValueGenerator#generateRandomValue RandomValueGenerator#generateRandomValue(type)は以下のようになります。 RandomValueGenerator package jp.small_java_world.dummydatafactory; import java.sql.Timestamp; import java.util.Date; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; public class RandomValueGenerator { public static final int DAFAULT_DATA_SIZE = 10; public static Object generateRandomValue(Class<?> type) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil.generateRandomString(DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } } if (type.isAssignableFrom(String.class)) { のようにtypeで分岐し、RandomDataUtilのtypeに対応するメソッドを呼び出し値を返却しています。 現時点では、Stringのサイズは10固定としています。 Stringはプリミティブ型が存在しないのでこれでOKなのですが、Integerのようにプリミティブ型が存在する場合は、以下のようにtype.getName().equals("int")のor条件が必要となります。 } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { RandomValueGenerator#generateRandomValueのテスト RandomValueGeneratorTargetDtoを対象クラスとするテストとなります。 RandomValueGeneratorTest package jp.small_java_world.dummydatafactory; import static org.junit.jupiter.api.Assertions.assertEquals; import java.sql.Timestamp; import java.util.Calendar; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.entity.RandomValueGeneratorTargetDto; import jp.small_java_world.dummydatafactory.util.RandomDataUtil; import jp.small_java_world.dummydatafactory.util.ReflectUtil; class RandomValueGeneratorTest { private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberLong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { var targetField = ReflectUtil.getDeclaredField(RandomValueGeneratorTargetDto.class, testType.targetMemberName); try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomString(RandomValueGenerator.DAFAULT_DATA_SIZE); }).thenReturn(GenerateRandomValueTestType.STRING.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomLong(); }).thenReturn(testType == GenerateRandomValueTestType.LONG ? GenerateRandomValueTestType.LONG.mockResult : GenerateRandomValueTestType.PRIMITIVE_LONG.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomFloat(); }).thenReturn(testType == GenerateRandomValueTestType.FLOAT ? GenerateRandomValueTestType.FLOAT.mockResult : GenerateRandomValueTestType.PRIMITIVE_FLOAT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomShort(); }).thenReturn(testType == GenerateRandomValueTestType.SHORT ? GenerateRandomValueTestType.SHORT.mockResult : GenerateRandomValueTestType.PRIMITIVE_SHORT.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomBool(); }).thenReturn( testType == GenerateRandomValueTestType.BOOLEAN ? GenerateRandomValueTestType.BOOLEAN.mockResult : GenerateRandomValueTestType.PRIMITIVE_BOOLEAN.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomDate(); }).thenReturn(GenerateRandomValueTestType.DATE.mockResult); randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomTimestamp(); }).thenReturn(GenerateRandomValueTestType.TIMESTAMP.mockResult); var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); } } } RandomValueGeneratorTargetDto package jp.small_java_world.dummydatafactory.entity; import java.sql.Timestamp; import java.util.Date; public class RandomValueGeneratorTargetDto { String memberString; Integer memberInteger; int memberInt; Long memberLong; long memberlong; Float memberFloat; float memberfloat; Short memberShort; short membershort; Boolean memberBoolean; boolean memberboolean; Date memberDate; Timestamp memberTimestamp; public String getMemberString() { return memberString; } public void setMemberString(String memberString) { this.memberString = memberString; } public Integer getMemberInteger() { return memberInteger; } public void setMemberInteger(Integer memberInteger) { this.memberInteger = memberInteger; } public int getMemberInt() { return memberInt; } public void setMemberInt(int memberInt) { this.memberInt = memberInt; } public Long getMemberLong() { return memberLong; } public void setMemberLong(Long memberLong) { this.memberLong = memberLong; } public long getMemberlong() { return memberlong; } public void setMemberlong(long memberlong) { this.memberlong = memberlong; } public Float getMemberFloat() { return memberFloat; } public void setMemberFloat(Float memberFloat) { this.memberFloat = memberFloat; } public float getMemberfloat() { return memberfloat; } public void setMemberfloat(float memberfloat) { this.memberfloat = memberfloat; } public Short getMemberShort() { return memberShort; } public void setMemberShort(Short memberShort) { this.memberShort = memberShort; } public short getMembershort() { return membershort; } public void setMembershort(short membershort) { this.membershort = membershort; } public Boolean getMemberBoolean() { return memberBoolean; } public void setMemberBoolean(Boolean memberBoolean) { this.memberBoolean = memberBoolean; } public boolean isMemberboolean() { return memberboolean; } public void setMemberboolean(boolean memberboolean) { this.memberboolean = memberboolean; } public Date getMemberDate() { return memberDate; } public Timestamp getMemberTimestamp() { return memberTimestamp; } public void setMemberDate(Date memberDate) { this.memberDate = memberDate; } public void setMemberTimestamp(Timestamp memberTimestamp) { this.memberTimestamp = memberTimestamp; } } テストメソッドtestGenerateRandomValueは、ParameterizedTestのEnumSource指定となります。 RandomValueGeneratorTest @ParameterizedTest @EnumSource(GenerateRandomValueTestType.class) void testGenerateRandomValue(GenerateRandomValueTestType testType) throws NoSuchFieldException { GenerateRandomValueTestTypeは、RandomValueGeneratorTargetDtoの各メンバの名前と RandomValueGenerator.generateRandomValue(type:各メンバに対応する型); が呼びされた時の期待値を管理するenumとなります。 RandomValueGeneratorTest private enum GenerateRandomValueTestType { STRING("memberString", "dummyString"), INTEGER("memberInteger", 1), PRIMITIVE_INT("memberInt", 2), LONG("memberLong", 3L), PRIMITIVE_LONG("memberLong", 4L), FLOAT("memberFloat", 5f), PRIMITIVE_FLOAT("memberfloat", 6f), SHORT("memberShort", (short) 7), PRIMITIVE_SHORT("membershort", (short) 8), BOOLEAN("memberBoolean", true), PRIMITIVE_BOOLEAN("memberboolean", false), DATE("memberDate", Calendar.getInstance().getTime()), TIMESTAMP("memberTimestamp", new Timestamp(Calendar.getInstance().getTime().getTime())); private final String targetMemberName; private final Object mockResult; private GenerateRandomValueTestType(final String targetMemberName, Object mockResult) { this.targetMemberName = targetMemberName; this.mockResult = mockResult; } } RandomValueGeneratorTest try (var randomDataUtilMock = Mockito.mockStatic(RandomDataUtil.class)) { でRandomDataUtilをモック化し、GenerateRandomValueTestType testTypeのtargetMemberNameとmockResultでモックの振る舞いを定義し、 RandomValueGeneratorTest var result = RandomValueGenerator.generateRandomValue(targetField.getType()); assertEquals(testType.mockResult, result); でRandomValueGenerator.generateRandomValue(targetField.getType())の結果がtestTypeのmockResultと一致することを検証しています。 本テストでは、モックのverifyは行っていませんので、現在のテスト対象のEnum以外のモックの振る舞いも定義してますが RandomValueGeneratorTest randomDataUtilMock.when(() -> { RandomDataUtil.generateRandomInt(); }).thenReturn( // RandomDataUtil.generateRandomInt()はGenerateRandomValueTestType.INTEGERと // GenerateRandomValueTestType.PRIMITIVE_INT両方で呼ばれるので // 現在のtestTypeで結果を分岐しています。 testType == GenerateRandomValueTestType.INTEGER ? GenerateRandomValueTestType.INTEGER.mockResult : GenerateRandomValueTestType.PRIMITIVE_INT.mockResult); のようにRandomDataUtilの同一メソッドの振る舞いの分岐を行う必要があります。 これで「ランダムな値を含むインスタンスの生成ロジックの実装」は完了となります。 DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装 Create TableのSQLを解析し、JavaのString型のメンバの長さを決定する必要があります。 SQLのパスを特定 SQLを解析し、長さを特定 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 を実現していきます。 ロジックの実装に必要な処理の実装 Javaのプロジェクト/bin/main/パスから上位に上がって指定ディレクトリを探す処理 Create TableのSQLはバージョン管理システムで管理しており、ローカルのチェックアウト先のファイルをそのまま利用することを想定しております。テスト用のパスにコピーして利用では変更に弱いですので。 プロジェクト名がDummyDataFactoryの場合で、以下のようなディレクトリ構造の場合 DummyDataFactory/bin/main/  ├ ../../hoge1  ├ ../../../hoge2/fuga DirectoryUtil.getPath("hoge1") だとDummyDataFactory/bin/main/../../hoge1を返却 hoge1の直下に各テーブルのSQLがあるとの利用方法を想定しています。 DirectoryUtil.getPath("hoge2/fuga") だとDummyDataFactory/bin/main/../../../hoge1/fugaを返却 する処理となります。 DirectoryUtil DirectoryUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DirectoryUtil { private static final Logger logger = LoggerFactory.getLogger(DirectoryUtil.class); /** * dirNameに階層を指定する場合は/を指定してください。 * * @param dirName * @return 存在するdirNameのプロジェクト/bin/main/からの相対パス */ public static String getPath(String dirName) { logger.debug("getPath dirName={} start", dirName); URL url = DirectoryUtil.class.getClassLoader().getResource("."); if (url == null) { return null; } StringBuilder upperPathPrefix = new StringBuilder(".." + File.separator); var rootPath = url.getPath(); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if(SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } String currentTargetPath = null; int counter = 0; while (true) { currentTargetPath = rootPath + upperPathPrefix + dirName.replace("/", File.separator); if (Files.exists(Path.of(currentTargetPath))) { break; } if (counter > 5) { logger.error("getPath fail"); return null; } upperPathPrefix.append(".." + File.separator); counter++; } logger.debug("getPath result={}", currentTargetPath); return currentTargetPath; } } DirectoryUtilのテスト 期待値のディレクトリの作成後にテストを実施しております。ゴミディレクトリが残るのが・・・ DirectoryUtilTest package jp.small_java_world.dummydatafactory.util; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class DirectoryUtilTest { @ParameterizedTest @ValueSource(strings = { "hoge1", "hoge2/fuga" }) void testGetPath(String dirNameParam) throws IOException { URL url = DirectoryUtil.class.getClassLoader().getResource("."); var rootPath = url.getPath(); var dirName = dirNameParam.replace("/", File.separator); //Windowsの場合は/c:のようなパスになるので、Path.ofなどがうまく動作しないので先頭の/を除去 if (SystemUtil.isWindows() && rootPath.startsWith("/")) { rootPath = rootPath.substring(1, rootPath.length()); } //DirectoryUtil.getPathがパスを検出するパスを作成 var targetDir = rootPath + ".." + File.separator + ".." + File.separator + dirName; //targetDirが存在しない場合は作成 if(!Files.exists(Path.of(targetDir))) { //targetDirに対応するディレクトリを作成 Files.createDirectories(Path.of(targetDir)); } var result = DirectoryUtil.getPath(dirName); //rootPathはテスト実行環境に依存するので、resultのrootPathを$rootPathに置換 result = result.replace(rootPath, "$rootPath"); //期待値も$rootPathからの相対パスで宣言 var expectedResult = "$rootPath" + ".." + File.separator + ".." + File.separator + dirName; assertEquals(expectedResult, result); } } SystemUtilはよくある実装だと思いますので、不要かもしれないですが、以下のようになります。 SystemUtil package jp.small_java_world.dummydatafactory.util; public class SystemUtil { public static boolean isWindows() { return System.getProperty("os.name").toLowerCase().startsWith("windows"); } } SQLを解析する処理 SqlAnalyzer Create TableのSQLの内容のStringが引数で、戻り値がMapとなります。 戻り値のMapのキー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnDataとなります。 それにしても、カラムの長さだけしか利用しないのに、処理がオーバースペックすぎますね・・・、まあ他の用途での利用も想定されますので・・・ SqlAnalyzer package jp.small_java_world.dummydatafactory.util; import java.util.HashMap; import java.util.Map; import jp.small_java_world.dummydatafactory.config.ColumnTypeConfig; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.create.table.CreateTable; public class SqlAnalyzer { public static Map<String, SqlColumnData> getSqlColumnDataMap(String sqlContent) throws JSQLParserException { //キー:カラムに対応するJavaのクラスのメンバ名、値:カラムに対応するSqlColumnData Map<String, SqlColumnData> result = new HashMap<>(); //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); sqlColumnData.setDbDataType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setColumnName(columnDefinition.getColumnName()); //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } result.put(sqlColumnData.getColumnCamelCaseName(), sqlColumnData); } return result; } } SqlAnalyzer //create tableのsqlContentを解析 CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sqlContent); JSqlParserを利用してCreate TableのSQLを解析しています。 createTable.getColumnDefinitions()で各カラムの解析結果を含んだListが取得できるので、SqlColumnDataのインスタンスを作成し、各カラムの結果を格納します。 SqlAnalyzer //解析結果からcolumnDefinitionsを取り出す。 for (var columnDefinition : createTable.getColumnDefinitions()) { SqlColumnData sqlColumnData = new SqlColumnData(); SqlColumnData package jp.small_java_world.dummydatafactory.data; public class SqlColumnData { String columnName; String columnCamelCaseName; String javaType; String dbDataType; Integer dbDataSize; public String getColumnName() { return columnName; } public void setColumnName(String columnName) { this.columnName = columnName; } public String getColumnCamelCaseName() { return columnCamelCaseName; } public void setColumnCamelCaseName(String columnCamelCaseName) { this.columnCamelCaseName = columnCamelCaseName; } public String getJavaType() { return javaType; } public void setJavaType(String javaType) { this.javaType = javaType; } public String getDbDataType() { return dbDataType; } public void setDbDataType(String dbDataType) { this.dbDataType = dbDataType; } public Integer getDbDataSize() { return dbDataSize; } public void setDbDataSize(Integer dbDataSize) { this.dbDataSize = dbDataSize; } } columnDefinition.getColDataType().getDataType()でカラムのデータタイプ(character varyingやdateなど)が取得できますので、これをJavaの型に変換し、sqlColumnDataのjavaTypeに格納しています。 SqlAnalyzer //javaTypeはcolumnType.ymlに定義してある設定で変換してセット var javaType = ColumnTypeConfig.getJavaType(columnDefinition.getColDataType().getDataType()); sqlColumnData.setJavaType(javaType); ColumnTypeConfig.getJavaType("character varying")だと"String"が返却されます。 ColumnTypeConfig.getJavaType("bigint")だと"Long"が返却されます。 columnType.ymlの内容は以下のようになっています。いろいろ足りてないですが・・・ columnType.yml character varying: String character: String integer: Integer timestamp: Timestamp timestamp with time zone: Timestamp smallint: Short date: Date bigint: Long ColumnTypeConfigですが、static初期化ブロックでcolumnType.ymlを読み込んでCOLUMN_TYPE_MAPを初期化し、 ColumnTypeConfig#getJavaTypeで指定された文字列をキーとするCOLUMN_TYPE_MAPのエントリーの値を返却しております。 ColumnTypeConfig package jp.small_java_world.dummydatafactory.config; import java.io.InputStream; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; public class ColumnTypeConfig { private static final Logger logger = LoggerFactory.getLogger(ColumnTypeConfig.class); private static Map<?, ?> COLUMN_TYPE_MAP; static { InputStream inputStream = ColumnTypeConfig.class.getResourceAsStream("/columnType.yml"); Yaml yaml = new Yaml(); COLUMN_TYPE_MAP = yaml.loadAs(inputStream, Map.class); } public static String getJavaType(String dbDataType) { if (COLUMN_TYPE_MAP.containsKey(dbDataType)) { logger.debug("dbDataType={} javaType={}", dbDataType, COLUMN_TYPE_MAP.get(dbDataType)); return COLUMN_TYPE_MAP.get(dbDataType).toString(); } else { logger.error("dbDataType={} javaType is not defined", dbDataType); } return null; } } カラム名に対応するJavaのクラスのメンバ名を生成し、sqlColumnDataのcolumnCamelCaseNameにセットしています。 SqlAnalyzer //Javaのクラスのメンバ名はテーブルのカラム名をキャメルケースに変換してセット sqlColumnData.setColumnCamelCaseName(StringConvertUtil.toSnakeCaseCase(columnDefinition.getColumnName())); StringConvertUtil package jp.small_java_world.dummydatafactory.util; import org.apache.commons.lang3.StringUtils; public class StringConvertUtil { public static String toSnakeCaseCase(String snakeCase) { StringBuilder stringBuilder = new StringBuilder(snakeCase.length() + 10); if (StringUtils.isEmpty(snakeCase)) { return snakeCase; } String firstUpperCase = firstLowerCase(snakeCase); String[] terms = StringUtils.splitPreserveAllTokens(firstUpperCase, "_", -1); stringBuilder.append(terms[0]); for (int i = 1, len = terms.length; i < len; i++) { stringBuilder.append(firstUpperCase(terms[i])); } return stringBuilder.toString(); } public static String toCamelCase(String camelCase) { if (StringUtils.isEmpty(camelCase)) { return camelCase; } StringBuilder stringBuilder = new StringBuilder(camelCase.length() + 10); String firstLowerCase = firstLowerCase(camelCase); char[] buf = firstLowerCase.toCharArray(); stringBuilder.append(buf[0]); for (int i = 1; i < buf.length; i++) { if ('A' <= buf[i] && buf[i] <= 'Z') { stringBuilder.append('_'); stringBuilder.append((char) (buf[i] + 0x20)); } else { stringBuilder.append(buf[i]); } } return stringBuilder.toString(); } private static String firstLowerCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toLowerCase().concat(target.substring(1)); } } public static String firstUpperCase(String target) { if (StringUtils.isEmpty(target)) { return target; } else { return target.substring(0, 1).toUpperCase().concat(target.substring(1)); } } } columnDefinition.getColDataType().getArgumentsStringList().get(0)にカラムサイズが格納されているので、sqlColumnDataのdbDataSizeに格納しています。 SqlAnalyzer //カラムサイズをセット var argumentsStringList = columnDefinition.getColDataType().getArgumentsStringList(); if (argumentsStringList != null) { sqlColumnData.setDbDataSize(Integer.parseInt(argumentsStringList.get(0))); } これで、JavaのクラスのString型のメンバの最大長を決定できます。 ランダムな値(Stringの長さ考慮)を含むインスタンスの生成 DummyDataFactory#generateDummyInstanceを以下のように変更します。 isEntity=trueで呼び出すと、対応するSQLを読み込み、対象クラスのインスタンスを生成します。 DummyDataFactory public class DummyDataFactory { private static final Logger logger = LoggerFactory.getLogger(DummyDataFactory.class); public static <T> T generateDummyInstance(Class<T> targetClass, boolean isEntity) throws Exception { // targetClassとその親のフィールドを取得 Field[] ownFields = targetClass.getDeclaredFields(); Field[] superFields = targetClass.getSuperclass().getDeclaredFields(); // targetClassとその親のフィールドをfieldsにセット Field[] fields = new Field[ownFields.length + (superFields != null ? superFields.length : 0)]; System.arraycopy(ownFields, 0, fields, 0, ownFields.length); if (superFields != null) { System.arraycopy(superFields, 0, fields, ownFields.length, superFields.length);// } // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } // 生成対象のクラスのインスタンスを生成 Constructor<T> constructor = targetClass.getDeclaredConstructor(); constructor.setAccessible(true); T entity = constructor.newInstance(); // 対象フィールドをループし、フィールドに対応するダミーデータの生成とentityへのセットを行う。 for (Field field : fields) { Class<?> type = field.getType(); String fieldName = field.getName(); int modifiers = field.getModifiers(); if (Modifier.isFinal(modifiers)) { continue; } // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); try { field.setAccessible(true); field.set(entity, fieldValue); } catch (Exception e) { logger.info("Exception occurred in generateTestEntity ", e); logger.error("set value fail entityClass={} fieldName={} fieldValue={}", targetClass.getPackageName(), fieldName, fieldValue); } } return entity; } } 変更点としては、 DummyDataFactory // キー:Field#getName()、値:SqlColumnData // 厳密にはキーはdbのカラム名をキャメルケースに変換した値 Map<String, SqlColumnData> sqlColumnDataMap = new HashMap<>(); // isEntity=trueの場合は対応するcreate sqlの中身を読み込んでMap<String, SqlColumnData>を生成 if (isEntity) { String sqlContent = SqlFileUtil.getSqlContent(targetClass.getSimpleName()); if (sqlContent != null) { sqlColumnDataMap = SqlAnalyzer.getSqlColumnDataMap(sqlContent); } } と、RandomValueGenerator.generateRandomValueへ、SqlColumnDataの引数追加 DummyDataFactory // 実際のダミーデータの生成 Object fieldValue = RandomValueGenerator.generateRandomValue(type, sqlColumnDataMap.get(fieldName)); となります。 SqlFileUtilは以下のようになります。 SqlFileUtil package jp.small_java_world.dummydatafactory.util; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jp.small_java_world.dummydatafactory.config.CommonConfig; public class SqlFileUtil { private static final Logger logger = LoggerFactory.getLogger(SqlFileUtil.class); public static String getSqlContent(String targetClassSimpleName) throws IOException { //dummyDataFactorySetting.propertiesのsqlDirNameの値のディレクトリのパスを取得 var sqlFileDir = DirectoryUtil.getPath(CommonConfig.getSqlDirName()); var tableName = StringConvertUtil.toCamelCase(targetClassSimpleName).toLowerCase(); //dummyDataFactorySetting.propertiesのsqlFilePattern=create_$tableName.sql //からtargetClassSimpleNameに対応するsqlのファイル名を作成 var createSqlFileName = CommonConfig.getSqlFilePattern().replace("$tableName", tableName); var createSqlFilePath = Path.of(sqlFileDir + File.separator + createSqlFileName); //dummyDataFactorySetting.propertiesのsqlEndKeywordの値を取得 var sqlEndKeyword = CommonConfig.getSqlEndKeyword(); if (Files.exists(createSqlFilePath)) { var sqlContent = Files.readString(createSqlFilePath); //create indexなどを同じファイルに記載している場合解析に失敗するので、sqlEndKeywordの値以降は切り捨て if (StringUtils.isNotEmpty(sqlEndKeyword) && sqlContent.contains(sqlEndKeyword)) { sqlContent = sqlContent.substring(0, sqlContent.indexOf(sqlEndKeyword)); } logger.debug("getSqlContent return value {}", sqlContent); return sqlContent; } else { logger.error("not exist createSqlFile={}", createSqlFilePath); return null; } } } RandomValueGenerator#generateRandomValueを以下のように変更します。 RandomValueGenerator public static Object generateRandomValue(Class<?> type, SqlColumnData sqlColumnData) { if (type.isAssignableFrom(String.class)) { return RandomDataUtil .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); } else if (type.isAssignableFrom(Integer.class) || type.getName().equals("int")) { return RandomDataUtil.generateRandomInt(); } else if (type.isAssignableFrom(Long.class) || type.getName().equals("long")) { return RandomDataUtil.generateRandomLong(); } else if (type.isAssignableFrom(Float.class) || type.getName().equals("float")) { return RandomDataUtil.generateRandomFloat(); } else if (type.isAssignableFrom(Short.class) || type.getName().equals("short")) { return RandomDataUtil.generateRandomShort(); } else if (type.isAssignableFrom(Boolean.class) || type.getName().equals("boolean")) { return RandomDataUtil.generateRandomBool(); } else if (type.isAssignableFrom(Date.class)) { return RandomDataUtil.generateRandomDate(); } else if (type.isAssignableFrom(Timestamp.class)) { return RandomDataUtil.generateRandomTimestamp(); } return null; } RandomValueGenerator#generateRandomValueの変更点 - .generateRandomString(DAFAULT_DATA_SIZE); + .generateRandomString(sqlColumnData != null ? sqlColumnData.getDbDataSize() : DAFAULT_DATA_SIZE); DummyDataFactory#generateDummyInstanceのテスト DummyDataFactoryTest package jp.small_java_world.dummydatafactory; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import jp.small_java_world.dummydatafactory.data.SqlColumnData; import jp.small_java_world.dummydatafactory.entity.DummyEntity; import jp.small_java_world.dummydatafactory.entity.FugaEntity; import jp.small_java_world.dummydatafactory.entity.HogeEntity; import jp.small_java_world.dummydatafactory.util.ReflectUtil; import jp.small_java_world.dummydatafactory.util.SqlAnalyzer; import jp.small_java_world.dummydatafactory.util.SqlFileUtil; class DummyDataFactoryTest { @ParameterizedTest @ValueSource(strings = { "true", "false" }) void testGenerateDummyInstance(boolean isEntity) throws Exception { var integerMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "integerMember"); var shortMemberMemberField = ReflectUtil.getDeclaredField(DummyEntity.class, "shortMember"); try (var randomValueGeneratorMock = Mockito.mockStatic(RandomValueGenerator.class); var sqlFileUtilMock = Mockito.mockStatic(SqlFileUtil.class); var sqlAnalyzerMock = Mockito.mockStatic(SqlAnalyzer.class)) { SqlColumnData integerMemberSqlColumnData = isEntity ? new SqlColumnData() : null; SqlColumnData shortMemberSqlColumnData = isEntity ? new SqlColumnData() : null; if (isEntity) { // SqlFileUtil.getSqlContent("dummyEntity")の振る舞いを定義 sqlFileUtilMock.when(() -> { SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()); }).thenReturn("dummy create sql"); // SqlAnalyzer.getSqlColumnDataMap("dummy create sql")の振る舞いを定義 sqlAnalyzerMock.when(() -> { SqlAnalyzer.getSqlColumnDataMap("dummy create sql"); }).thenReturn( Map.of("integerMember", integerMemberSqlColumnData, "shortMember", shortMemberSqlColumnData)); } // DummyEntityのintegerMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData); }).thenReturn(100); // DummyEntityのshortMemberに対するダミーデータの生成時の振る舞いを定義 randomValueGeneratorMock.when(() -> { RandomValueGenerator.generateRandomValue(shortMemberMemberField.getType(), shortMemberSqlColumnData); }).thenReturn((short) 101); // DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity)を呼び出して値の検証 var result = DummyDataFactory.generateDummyInstance(DummyEntity.class, isEntity); assertThat(result.getIntegerMember()).isEqualTo(100); assertThat(result.getShortMember()).isEqualTo((short) 101); // モックのverify // SqlFileUtil.getSqlContentとSqlAnalyzer.getSqlColumnDataMapはisEntity=trueのときのみ呼び出される。 sqlFileUtilMock.verify(() -> SqlFileUtil.getSqlContent(DummyEntity.class.getSimpleName()), times(isEntity ? 1 : 0)); sqlAnalyzerMock.verify(() -> SqlAnalyzer.getSqlColumnDataMap("dummy create sql"), times(isEntity ? 1 : 0)); randomValueGeneratorMock.verify(() -> RandomValueGenerator.generateRandomValue(integerMemberField.getType(), integerMemberSqlColumnData), times(1)); randomValueGeneratorMock.verify(() -> RandomValueGenerator .generateRandomValue(shortMemberMemberField.getType(), shortMemberSqlColumnData), times(1)); } } } SqlAnalyzerのテスト SqlAnalyzerTest package jp.small_java_world.dummydatafactory.util; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.JSQLParserException; class SqlAnalyzerTest { @Test void testGetSqlColumnDataMap() throws JSQLParserException { var result = SqlAnalyzer.getSqlColumnDataMap("CREATE TABLE todo( " + " id integer not null," + " title character varying(32)," + " content character varying(100)," + "limit_time timestamp with time zone," + "regist_date date" + ");"); assertThat(result).hasSize(5); assertThat(result).containsKeys("id", "content", "title", "limitTime", "registDate"); String targetKey = "id"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Integer"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("integer"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "title"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(32); targetKey = "content"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("String"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("character varying"); assertThat(result.get(targetKey)).extracting("dbDataSize").isEqualTo(100); targetKey = "limitTime"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("limit_time"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Timestamp"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("timestamp with time zone"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); targetKey = "registDate"; assertThat(result.get(targetKey)).extracting("columnName").isEqualTo("regist_date"); assertThat(result.get(targetKey)).extracting("columnCamelCaseName").isEqualTo(targetKey); assertThat(result.get(targetKey)).extracting("javaType").isEqualTo("Date"); assertThat(result.get(targetKey)).extracting("dbDataType").isEqualTo("date"); assertThat(result.get(targetKey)).extracting("dbDataSize").isNull(); } } 完成版の全ソースは GitHubリポジトリ に登録しております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Boot Starter for Azure Storage を使ってみる

Spring Boot Starter for Azure Storage を使ってみる 自身でもちょっと混乱したので、備忘録代わりの記事です。Spring Boot for Azure Storage で検索すると、以下のサイトがヒットしたりしますが、この spring-starter-azure-storage ライブラリはすでに Deprecated です。 Azure Storage 用の Spring Boot Starter の使用方法 | Microsoft Docs 最新は名前も変って azure-spring-boot-starter-storage となってます。現在の最新のライブラリは、基本的に azure-spring-boot-starter で始まっています(一部古いものもありますが) 使ってみる このライブラリでは、BlobServiceClientBuilder を DIする方法と、Valueアノテーションを使って、リソースをDIする方法が提供されています。はじめに、BlobServiceClientBuilder を DI する方法を試してみましょう。 https://start.spring.io/ からの生成に対応しているので、そちらを使うと便利ですが、pom.xml の 依存関係には以下が必要です。 <dependency> <groupId>com.azure.spring</groupId> <artifactId>azure-spring-boot-starter-storage</artifactId> </dependency> そして、application.properties には、 ストレージアカウント名、アカウントキー、エンドポイント名が必要です。 azure.storage.accountName=<<your account name>> azure.storage.accountKey=<<your account key>> azure.storage.blob-endpoint=https://<<account name>>.blob.core.windows.net/ コンストラクタインジェクションでも、フィールドインジェクションでもどちらも構いません。以下はコンストラクタインジェクションで試しています。BlobServiceClientBuilder からクライアントインスタンスをビルドするのですが、同期クラアントでも非同期クライアントでもどちらでもよいでしょう。Sprinb Boot WebFlux は、Project Reactor による非同期プログラミングをサポートしているので、以下では非同期クライアントをビルドしています。Azure Storage SDK for Java 自身が、Reactorによる非同期に対応しているので、相性は良いと思います。 以下は、ザックリとコンテナ一覧を表示している例です。listBlobContainers して コンテナ名を返しているところです。戻り値が、Mono<List<Strting>> で、Flux<String> ではないのは、WebFluxの仕様上の話です。Flux<String> で返すと、JSONのチャンクとして扱われてしまうようで、少しハマリました。 RestController public class BlobController { private final BlobServiceAsyncClient blobServiceAsyncClient; public BlobController(BlobServiceClientBuilder builder) { this.blobServiceAsyncClient = builder.buildAsyncClient(); } @GetMapping(value = "/containers", produces = MediaType.APPLICATION_JSON_VALUE) public Mono<List<String>> listContainers() { return this.blobServiceAsyncClient .listBlobContainers() .map(c -> c.getName()) .collectList(); } } もう1つの Resource に差し込む方法として、以下のようにValue アノテーションで、DIすることができます。形式は、azure-blob://コンテナ名/ブロブ名 になります。以下は、プロパティファイルで置換しています。 @Value("azure-blob://${containerName}/${blobName}") private Resource blobFile; @GetMapping("/blob") public String hello() throws IOException { return StreamUtils.copyToString( this.blobFile.getInputStream(), Charset.forName("UTF-8")); } ちなみに、blobFile.getInputStream() を呼ぶと、ライブラリ側でblock() されているらしく、WebFlux 上では例外が出て動きませでしたので注意が必要です。 イマイチ使いどころが分りませんが、もう少しpring フレームワークを知れば、なにか使いどころがあるのかもしれません。 まとめ プロパティに、ストレージキーが書かれていますが、Managed IDに対応しているかは、GithubのREADMEには書かれていませんでしたので不明です。時間があれば試してみたいと思います。 あと、サービス毎にストレージ分けたりした場合、複数のアカウントに対応してないっぽいので、それも使いにくいかもしれません。そのあたりのサービスはさくっと実装してもあまり手間がない気もします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

hashCode が同じはずの自作クラスが HashSet 内で重複する

TL;DR HashSet で自作クラスを使用する場合、 hashCode メソッドだけでなく equals メソッドも準備する必要があります。 確認した JDK openjdk version "1.8.0_265" OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_265-b01) OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.265-b01, mixed mode) 動作 次のように、各プロパティフィールドが同じ値を持つ場合は hashCode() の結果が同じになるようにしつつ、 equals は true とならないクラスを準備します。 public class MyModel { private String name; private int age; public MyModel(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { return false; // ちょうどよいサンプル書くのめんどうなので無理やり false します。 } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } import org.junit.Test; import java.util.HashSet; import static org.junit.Assert.*; public class MyModelTest { @Test public void testHashSet() { HashSet<MyModel> set = new HashSet<>(); // 同じプロパティを持つオブジェクトを2つ放り込む set.add(new MyModel("taro", 10)); set.add(new MyModel("taro", 10)); // hashCode が同じなのでサイズは 1 つになるはず assertEquals(1, set.size()); } } しかし結果はこうなります。 java.lang.AssertionError: Expected :1 Actual :2 <Click to see difference> なので hashCode と同じ挙動になるように equals をちゃんと実装すると、サイズは1になります。 public class MyModel { private String name; private int age; public MyModel(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyModel myModel = (MyModel) o; if (age != myModel.age) return false; return name != null ? name.equals(myModel.name) : myModel.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } なぜ 簡単に言えば、置き換えチェックに equals も使ってるからです。 具体的には HashSet は内部で HashMap を使用しており、セットする値は HashMap の key として取り扱っています。 なので HashMap の key 実装に依存するわけですね。 このあたりです。 HashSet::add HashMap::putVal あくまで OpenJDK の実装です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

hashCode が同じはずの自作クラスが HashSet 内で重複してしまう

TL;DR HashSet で自作クラスを使用する場合、 hashCode メソッドだけでなく equals メソッドも準備する必要があります。 確認した JDK openjdk version "1.8.0_265" OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_265-b01) OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.265-b01, mixed mode) 動かしてみる 次のように、各プロパティフィールドが同じ値を持つ場合は hashCode() の結果が同じになるようにしつつ、 equals は true とならないクラスを準備します。 public class MyModel { private String name; private int age; public MyModel(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { return false; // ちょうどよいサンプル書くのめんどうなので無理やり false します。 } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } import org.junit.Test; import java.util.HashSet; import static org.junit.Assert.*; public class MyModelTest { @Test public void testHashSet() { HashSet<MyModel> set = new HashSet<>(); // 同じプロパティを持つオブジェクトを2つ放り込む set.add(new MyModel("taro", 10)); set.add(new MyModel("taro", 10)); // hashCode が同じなのでサイズは 1 つになるはず assertEquals(1, set.size()); } } しかし結果はこうなります。 java.lang.AssertionError: Expected :1 Actual :2 なので hashCode と同じ挙動になるように equals をちゃんと実装すると、サイズは1になります。 public class MyModel { private String name; private int age; public MyModel(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyModel myModel = (MyModel) o; if (age != myModel.age) return false; return name != null ? name.equals(myModel.name) : myModel.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } なぜ Java SE 上ではあくまで「このセット内に、(e==null ? e2==null : e.equals(e2)) となる要素e2がない場合は、指定された要素eをこのセットに追加します。」というように、 hashCode でなく equals となってるようです。 "Hash"Set なのに… OpenJDK では HashSet は内部で HashMap を使用しており、セットする値は HashMap の key として取り扱っています。 なので HashMap の put の実装に依存するわけですね。 このあたりです。 HashSet::add HashMap::putVal
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java アルゴリズム修行⑭】クイックソート

クイックソートとは 前回の記事では、単純挿入ソートをアップグレードしたシェルソートについて学んでみましたが、 今回はその流れでクイックソートについて学んでみたいと思います! その名の通り、広く一般的に使われる高速なアルゴリズムであることからクイックソートと呼ばれていますが、 ざっくり言うと、中間位置を定めてその前後のグループ分けでソートを繰り返すといったようなものです。。 シェルソートでは等間隔のグループでソートを繰り返し、できる限りソート済の状態に近い形に持っていきましたが、 クイックソートでは、枢軸と呼ばれるグループ分けの基準のようなものを設けて、その前後でソートを行います。 と言葉にしてもイメージしにくので、実際に流れでやっていきましょう! {4,3,5,7,1,8,6,2} という数列を昇順にソートしたいとして、 この内の中間的な値である「4」を枢軸として、その前後でグループ分けします。 この「4」よりも大きいか小さいかでグループを二つに分けると、、 4よりも小さければ左に、大きければ右に寄せることで 4より小さいグループ:{3,1,2} 4より大きいグループ:{5,7,8,6} と分けることができました。 次に、小さいグループの中でも最初と同様に「2」を枢軸としてグループ分けします。 その「2」よりも大きいか小さいかでさらにグループ分けをすると、{1,2,3}という並びにすることができます 次に大きいグループも枢軸を「6」として同様のグループ分けを行います。 「6」よりも大きいか小さいかでさらにグループ分けすることで、{5,6,7,8}となりました。 このように、枢軸よりも大きいグループ、小さいグループという分け方を続けることで 結果的に{1,2,3,4,5,6,7,8}とソートを完了させることができました。 これがクイックソートの流れですが、コードで表現するとなると、 分割の手順(枢軸を決めて大小でグループ分けを行う)をどう実現するかが肝になってきそうです。。 コードでどう表現するのか? まずは分割する方法を考えていきたいところですが、基本的には下記2つの移動を実現させる必要がありそうです。 枢軸以下の要素を配列の左側(先頭寄り)に移動させる 枢軸以上の要素を配列の右側(末尾寄り)に移動させる {5,7,1,4,6,2,3,9,8}という数列 aで考えてみると 枢軸をx とし、中央の「6]をxとします。 左端のインデックスをpl(左カーソル) 右端のインデックスをpr(右カーソル)としたとき、下記のように言い換えることができそうです。 


a[pl] >= xが成立する要素が見つかるまでplを右方向に走査する 


a[pr] <= xが成立する要素が見つかるまでprを左方向に走査する 左カーソルが x(枢軸)以上になるまで右方向に 右カーソルがx(枢軸)以下になるまで左方向に 走査する必要があるので、 {5,7,1,4,6,2,3,9,8}に上記の操作を行うと、下記の時点でストップします。 
plはa[2]である 「7」 >= 「6」でストップ 
prはa[6]である「3」 <= 「6」でストップ  左カーソルは枢軸以上の時、右カーソルは枢軸以下の時にストップするということですね。 ここで、左右のカーソルが位置する要素a[2]の「7」とa[6]の「3」の値を交換します。 すると、枢軸以下の値が左側に、枢軸以上の値が右側に移動するので、{5,3,1,4,6,2,7,9,8}となります 

再び走査を続けると、左右のカーソルは plはa[4]である「6」 >=「6」でストップ、prはa[5]である「2」<= 「6」でストップしますね。 左右のカーソル位置の値を交換すると、{5,3,1,4,2,6,7,9,8}
となり、 さらに走査を続けると、plとprは交差するのでこの時点で交換を終えると、カーソル位置は先ほどの交換の次にいくので、pl[5],pr[4]となります。 枢軸である「6」より小さいか、大きいかで以下のグループに分けることができました。 {5,3,1,4,2}と{7,8,9} 枢軸以下のグループ a[0]…a[pl-1] 先頭インデックス~左カーソルの手前まで 枢軸以上のグループa[pr+1]… a[n-1] 右カーソルの次~末尾インデックス というグループ分けを行うことができます。 また枢軸と一致するグループが生まれる場合もあり、 例えば、{1,8,7,4,5,2,6,3,9}という数列で 「5」を枢軸にしたとすると、 plはa[1]である 「8」 >= 「5」でストップ prはa[7]である「3」 <= 「5」でストップ  → 交換を行い {1,3,7,4,5,2,6,8,9} plはa[2]である 「7」 >= 「5」でストップ prはa[5]である「2」 <= 「5」でストップ  → 交換を行い {1,3,2,4,5,7,6,8,9} plはa[4]である 「5」 >= 「5」でストップ prはa[4]である「5」 <= 「5」でストップ  → 交換を行い {1,3,2,4,5,7,6,8,9}となります 3回目の走査では、左右のカーソルが両方とも「5」でストップしており、 枢軸と一致する中央グループが登場することがわかります。 (分割完了時に、pl > pr + 1が成立するときのみ) この分割過程をコードに落とし込んでみましょう。 alog.java static void partition(int[] a, int n) { int pl = 0; // 左カーソル int pr = n - 1; // 右カーソル int x = a[n / 2]; // 枢軸(中央の要素) do { while (a[pl] < x) pl++; //左カーソルが枢軸未満の場合はplのインデックスを右に while (a[pr] > x) pr--; //右カーソルが枢軸より大きい場合はplのインデックスを左に //右カーソルが左カーソル以上の(交差してなければそのまま交換を行う) if (pl <= pr){ swap(a, pl++, pr--); } } while (pl <= pr); //交差するまで(グループ分けが完了する)繰り返す 枢軸の値は配列の中央に位置する値ということにして、
do-while文で分割を繰り返しています。 a[pl]左カーソルが枢軸未満まで plのインデックスを インクリメントして右に移動 a[pr]右カーソルが枢軸より大きくなるまで prのインデックスをデクリメントして左に移動という流れです。 先ほどの{1,8,7,4,5,2,6,3,9}であれば、 plは0から、prは9-1 で8から始まり、xはa[9/2]なので、5とします。 while(a[0] < 5) の間は plをインクリメントするので a[0]「1」 < 5 → a[1]「8」 < 5 でfalseなので pl は1となる while(a[8] > 5)の間はデクリメントするので、 a[8]「9」> 5 →a[7]「3」> 5 で falseなので pr は7となる if(1 <= 7)で交差はしていないので、 swap(a,1,7)で「8」「3」 の交換を行い、 {1,3,7,4,5,2,6,8,9}となります。 そのまま繰り返し処理の冒頭に戻りますが、 swapメソッドの引数でplは後置インクリメント、prは後置デクリメントしているので plは 2 、prは6から始まります。 while(a[2] < 5) の間は plをインクリメントするので a[2]「7」 < 5 → でfalseなので pl は2となる while(a[6] > 5)の間はデクリメントするので、 a[6]「6」> 5 → a[5]「2」 > 5でfalseなのでplは5となる if(2 <= 5)で交差はしていないので、 swap(a,2,5)で「7」「2」 の交換を行い、 {1,3,2,4,5,7,6,8,9}となります。 といったように、先ほどは文章で表した分割の流れをコードで再現することができました! さらに、この配列分割を発展させてクイックソートを実現していきましょう。。! コードに落とし込んでみる 先ほどの分割コードで、枢軸よりも大きいグループ、小さいグループで2つに分けることができましたが、 この分割後のグループにも最初の分割と同じ手続きを踏ませることで、クイックソートを実現できそうです。 algo.java static void quickSort(int[] a, int left, int right) { int pl = left; // 左カーソル int pr = right; // 右カーソル int x = a[(pl + pr) / 2]; // 枢軸(中央の要素) do { while (a[pl] < x) pl++; while (a[pr] > x) pr--; if (pl <= pr) swap(a, pl++, pr--); } while (pl <= pr); if (left < pr) quickSort(a, left, pr); if (pl < right) quickSort(a, pl, right); } もし

要素数が1つしかないグループは、それ以上の分割は不要なので、
 再分割を適用するのは要素数が2以上のグループとしたとき、 prが先頭より右側に位置する状態(left < pr)であれば、leftからprまでのグループで再度ソート 
plが末尾より左側に位置する状態(pl < right)であれば、plからrightまでのグループで再度ソート つまり、prが先頭まで、plが末尾まで来た場合それ以上は分割する必要がないということになります。 これを繰り返し行うので、再帰呼び出しで実現させることができるということですね! {5,8,4,2,6,1,3,9,7}で実際にの処理過程を見てみましょう。 最初の呼び出しては quickSort(配列、0,8) として、左端が0、右端が8というインデックスが渡されます
。 そのまま0が左カーソルのpl、8が右カーソルprとして、枢軸であるxはa[8/2]の6となります。 while(a[0] < 6) の間は plをインクリメントするので a[0]「5」 < 6 → a[1]「8」 < 5 でfalseなので pl は1となります。 while(a[8] > 6)の間はprをデクリメントするので、 a[8]「7」> 6 →a[7]「9」> 6 → a[6] 「3」 > 6でfalseなので pr は6となります。 
1 <= 6で交差はしていないので、そのままa[1]「8」,a[6]「3」を交換し  →{5,3,4,2,6,1,8,9,7}として 

plが2prは5となり、 2 <= 5 はtrueなので、再度走査を続けます。 while(a[2] < 6) の間はplをインクリメントするので a[2]「4」 < 6 → a[3]「2」 < 6 → a[4] 「6」 < 6 でfalseなのでplは4となります。 while(a[5] > 6)の間はprをデクリメントするので、 a[5]「1」> 6 で falseなのでprは5となります。 4 <= 5 で交差はしていないので、そのままa[4]「6」とa[5]「1」を交換し →{5,3,4,2,1,6,8,9,7}
 として、でplは5、prは4となり 5 <= 4 はfalseとなるので、while文を抜け、次のif文に移行します。 if (left < pr) で left 「0」 < 4 であれば再帰的に呼び出す → quickSort(a,0,4)で左グループのソート if (pl < right) で 5 < right「8」であれば再帰的に呼び出す  → quickSort(a, 5, 8)で右グループのソート となるので、 左a[0] ~ a[4]{5,3,4,2,1} 右a[5] ~ a[8] {6,8,9,7}という2グループでそれぞれ同様の操作を行うことになる。 ■{5,3,4,2,1}の場合 plは0 prは4で、枢軸は{5,3,4,2,1,6,8,9,7}
のa[(0 + 4)/2]である「4」となります。 while(a[0] < 4) の間は plをインクリメントするので a[0]「5」 < 4 でfalseなのでplは0となります。 while(a[4] > 4)の間はデクリメントするので、 a[4]「1」> 4 でfalseなのでprは4のまま 0 <= 4で a[0]「5」 とa[4] 「1」を交換し、{1,3,4,2,5} そのまま plは1 pr は3となります。 while(a[1] < 4) の間は plをインクリメントするので a[1]「3」 < 4 → a[2]「4」 < 4 でfalseなので pl は2ととなります。 while(a[3] > 4)の間はデクリメントするので、 a[3]「2」> 4 でfalseなのでprは3となり、

2 <= 3  で a[2]「4」とa[3]「2」を交換し {1,3,2,4,5}
plは3 pr は2となります。 while(a[3] < 4) の間は plをインクリメントするので a[3]「4」 < 4 はfalseなので pl は3となります。 while(a[2] > 4)の間はデクリメントするので、 a[2]「2」> 4 はfalseなのでprは2となる 

3 <= 2 はfalseとなるのでwhile文を抜け、次のif文に移行します。 if (left < pr) で left 「0」 < 2 であれば再帰的に呼び出す → quickSort(a,0,2)で左グループのソート if (pl < right) で 3 < right「4」であれば再帰的に呼び出す →  quickSort(a, 3, 4)で右グループのソートを {5,3,4,2,1}から、左は{1,3,2}右は{4,5}とグループ分けされます。 ■ {6,8,9,7}の場合 pl が5 prが8で、枢軸は{1,3,2,4,5,6,8,9,7}
のa[(5+8)/2]である「8」となります while(a[5] < 8) の間は plをインクリメントするので a[5]「6」 < 8 → a[6] 「8」< 6 falseなので pl は6となります。 while(a[8] > 8)の間はデクリメントするので、 a[8]「7」> 8 はfalseなのでprは8となります。 
6 <= 8なので、そのままa[6]「8」とa[8]「7」を交換して{6,7,9,8}となり、plは 7 prは7 なので while(a[7] < 8) の間は plをインクリメントするので a[7]「9」 < 8 → falseなので pl は7となります。 while(a[7] > 8)の間はデクリメントするので、 a[7]「9」> 8 → a[6]「7」 > 8 でfalseなのでprは6となります。 
7 <= 6ではないのでここでwhile文を抜け、次のif文に移行します。 if (left < pr) で left 「5」 < 6 であれば再帰的に呼び出す → quickSort(a,5,6)で左グループのソート if (pl < right) で 7 < right「8」であれば再帰的に呼び出す →  quickSort(a, 7, 8)で右グループのソートを {6,7}{9,8}でそれぞれソートを行う。。という繰り返しを行うことで、クイックソートの手順を実現させることができそうです。 グループ分けをしてソートしているという表現になりますが、実際は常に配列aをquicksortに渡しているので、 着目する範囲のインデックスをそれぞれ前半と後半に分けてソートしているだけで、実際に分割しているわけではなさそうですね。。 ちなみに、クイックソートは隣接していない離れた要素を交換するので不安定という点には注意が必要です! 学んだこと 枢軸を選択し、その大小でソート範囲を決めていくので、ソートする範囲は常に近い値で2分されるためクイックなソートが可能 先頭、末尾からカーソルを動かすことで、大小のグループ分けを行える 配列を操作する上では、インデックスで着目範囲を決めれば分割という操作を実現することができる 使用機会が多いクイックソートですが、分割をどうやって表現するんだろう。。と悩みましたが カーソルと、着目要素をずらしていき、再帰的に処理することで実現できスッキリできました! インデックスを変えれば、事実上分割しているかのように処理できるので、この考え方は 今後も使っていきたいですね。。引き続き頑張っていきます! ※記事内の画像はITを分かりやすく解説から拝借しております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

internメソッドの概要

internメソッドの概要 先日Java Silverの学習を進めていた時にinternメソッドの問題で少し躓きました。 解説書を読みある程度まで理解が進んだのでメソッドの機能をまとめておこうと思います。 internメソッドとは java.lang.Stringクラスに属するメソッドでコンスタントプールを含んだメモリ領域内の文字列を探し、同じ文字列が存在すれば同じ文字列を返すもの。 仮に同じ文字れるが見つからなければ新たに文字列を生成します。 例 String s1 = "Hello"; System.out.println(s1.intern()); 出力結果 Hello となります。 簡単に言うとString型で使いたい文字列が既に生成されているならそこの参照を使いまわす、といったイメージだと思います。 ここまでは同じ文字を返すだけだと思っていたのですが、このメソッドはあくまで「既に存在している文字列の参照」を返すものです。 例えばこのような問題 String s1 = new String("Hello"); String s2 = "Hello"; String s3 = s1.intern(); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s2 == s3); 出力結果 false false true となります。 まず(s1 == s2)に関しては、 s1はnewキーワードによるインスタンス化が行われているため(ヒープ領域というクラス外の領域に文字列が保存される)、直接変数に文字列を代入したs2とは参照が違うため結果が「false」となります。 次に(s1 == s3)ですが、 s3は前述のとおり同じ文字列が既に使われているかを判断します。 このクラスでは「Hello」という文字列はまだ使われていません。(s1はヒープ領域に保存されているため見つけることができない) そのため、s3には新しく生成された文字列「Hello」が代入されることになります。 よって結果は「false」となります。 最後に(s2 == s3)、 ここではinternメソッドを使用しています。 この行の動きを説明すると「s1と同じ文字列をクラス内の領域で探し、同じものが既にあればその文字列の参照をs3に代入する」と言えます。 s2にすでに「Hello」という文字列が代入されている(クラスのメモリ領域に保存されている)ためinternメソッドはメモリから「Hello」という文字列を見つけ出しそれをs3へと代入します。 つまりこの結果は「true」となります。 まとめ internメソッドは同クラスのメモリ領域に同じ文字列が無いか探しに行くメソッド。 同じものが存在していればその文字列の「参照」を返す点に注意。 あくまで参照を返しているため同じ値ではないことも注意。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Effective Java(第3版) 項目8 ファイナライザとクリーナーを避ける

ファイナライザ、クリーナは一般には不要なため、使用することを避けるべき、という内容です。 記載内容 クリーナー、ファイナライザ共通 クリーナー、ファイナライザが実行されるまでの時間は予想できない 実行までの時間的な制約がある処理をクリーナー、ファイナライザで行うべきではない。 例えばファイルのクローズをクリーナー、ファイナライザに頼るべきではない。 クリーナー、ファイナライザの実行が遅れると、オープン可能なファイル数の限界に達し、 新しいファイルのオープンでエラーになる可能性がある。 クリーナー、ファイナライザを実装するよりも、 AutoCloseableを実装し、使用側でtry-with-resourcesを使うべきである。 クリーナーの正当な使い方は二つある。 クローズを呼び忘れた場合の保険 ネイティブピアの回収 クリーナー、ファイナライザの実行は言語仕様上は保証されていない DBのロック開放の様な、永続的な状態の更新をクリーナー、ファイナライザで行うべきではない。 パフォーマンスの劣化 クリーナー、ファイナライザを使用した場合、GCに関して深刻なパフォーマンスの劣化が発生する。 著者の環境では クリーナー、ファイナライザ呼び出しと伴うGC処理は50倍程度、 クリーナを保険として使用している場合でも5倍程度遅くなる。 ファイナライザ ファイナライザはJava9より非推奨になっており、使用するべきではない。 ファイナライザはセキュリティ問題を抱えており、悪意のあるサブクラスでファイナライザを実装することで、 不正なオブジェクト参照を取得することが可能になる。 これを防ぐには、クラスをfinalにするか、何もしないfinalのファイナライザを書けば良い。 クリーナー クリーナーはファイナライザよりは良いが、動作は予想不可能で遅く、また、一般的には使用する必要はない。 クリーナーのAPIは少々扱いづらいが、クラスのpublicのAPIに影響しない。 例として、AutoCloseableを実装したクラスに対して、 クローズ漏れに対する保険としてクリーナーを使用するのは、問題はない。 ただし、クリーナーが確実に実行されるとは限らない。 System.exitや、通常のプログラム終了に対するクリーナーの動作は実装に依存し、 実行は保証されない。 考察 かなり以前から、ファイナライザは使うべきではない、というのが一般的だったと思いますが、 非推奨になっており、明確に使うべきではない、という状態になっています。 ただし、代替となるクリーナーも、セキュリティ以外のファイナライザの欠点である 実行タイミングが分からない 確実に実行されるとは限らない はそのままですので、やはり確実な後処理には向かないです。 try-with-resourcesを使い、ファイルのオープンのように、使用上限がある重要な資源であれば、 保険でクリーナーを使用する、程度の使い方にとどまるかと思います。 ただし、GCに関するパフォーマンスの問題がありますので、AutoCloseableなクラス全てで クリーナーを使う、というのは避けるべきかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む