LoginSignup
2
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-05-06

はじめに

Rubyのgemの「FactoryBot」ですが、超有名プロダクトであることは衆目の一致するところです。
旧名称は「FactoryGirl」でしたので、こちらの方がピンとくる方も多いと思います。

テスト実装するときに、モックが返却する結果は手作業で実装することがほとんどだと思います。この実装が面倒ですよね。簡単な回避方法としては、共通化して、使いまわすぐらいでしょうか。

このテストデータの作り方の違いによって、テストが成功したり、失敗したりすることがあります。そんな場合は、テストの実装方法やプロダクトコードに問題がある場合が大半なのですが、せっかく実装したテストがテストデータ依存って、こんな悲しいことはありません。

テストを実行するたびに、新しいテストデータが生成され、テストデータ依存にならないテストが実装できればなーー、となります。

そんなときは「FactoryBot」です!
ってJavaなんですけど・・・
・・・

モックの戻り値の生成であれば、文字列の長さはそれほど問題にはなりませんが、DBに前提データとしてセットする場合は、長さの上限も重要となります。

本投稿ではカバーできていないのですが、コードや区分なども考慮しないと、実際のプロダクトコードのテストでストレスなく利用することは難しいので、この部分に関しては、今後の課題と考えております。

完成版の全ソースは
GitHubリポジトリ
に登録しております。

目的

以下を実現することを目的とします。

  • ランダムな値の生成ロジックの実装
  • ランダムな値を含むインスタンスの生成ロジックの実装
  • DBの前提データとして利用可能なランダムな値を含むインスタンスの生成ロジックの実装

利用するライブラリ

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(integerMemberField.getName(), integerMemberSqlColumnData,
                        shortMemberField.getName(), 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).getColumnName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getColumnCamelCaseName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getJavaType()).isEqualTo("Integer");
        assertThat(result.get(targetKey).getDbDataType()).isEqualTo("integer");
        assertThat(result.get(targetKey).getDbDataSize()).isNull();

        targetKey = "title";
        assertThat(result.get(targetKey).getColumnName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getColumnCamelCaseName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getJavaType()).isEqualTo("String");
        assertThat(result.get(targetKey).getDbDataType()).isEqualTo("character varying");
        assertThat(result.get(targetKey).getDbDataSize()).isEqualTo(32);

        targetKey = "content";
        assertThat(result.get(targetKey).getColumnName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getColumnCamelCaseName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getJavaType()).isEqualTo("String");
        assertThat(result.get(targetKey).getDbDataType()).isEqualTo("character varying");
        assertThat(result.get(targetKey).getDbDataSize()).isEqualTo(100);

        targetKey = "limitTime";
        assertThat(result.get(targetKey).getColumnName()).isEqualTo("limit_time");
        assertThat(result.get(targetKey).getColumnCamelCaseName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getJavaType()).isEqualTo("Timestamp");
        assertThat(result.get(targetKey).getDbDataType()).isEqualTo("timestamp with time zone");
        assertThat(result.get(targetKey).getDbDataSize()).isNull();

        targetKey = "registDate";
        assertThat(result.get(targetKey).getColumnName()).isEqualTo("regist_date");
        assertThat(result.get(targetKey).getColumnCamelCaseName()).isEqualTo(targetKey);
        assertThat(result.get(targetKey).getJavaType()).isEqualTo("Date");
        assertThat(result.get(targetKey).getDbDataType()).isEqualTo("date");
        assertThat(result.get(targetKey).getDbDataSize()).isNull();
    }

}

完成版の全ソースは
GitHubリポジトリ
に登録しております。

2
4
0

Register as a new user and use Qiita more conveniently

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