【Java】オブジェクトのフィールドの値を比較するテスト

【Java】オブジェクトのフィールドの値を比較するテスト

同じオブジェクトのインスタンスが2つある時に、フィールドを含めてインスタンスに同じ値が設定されているかどうかを検証する話。

lombokのアノテーションでequalsをoverride

オブジェクト内にフィールドを一つ一つ検証するようなメソッドを作らなくても、lombokの@Data@EqualsAndHashCodeアノテーションを追加することでequalsメソッドがoverrideされ、オブジェクトのフィールドの値まで同じか検証してくれるようになります。lombok便利。

Address.java

01
02
03
04
05
06
07
08
09
10
11
12
@Builder
@Data
public class Address {
    private int addressId;
    private String address;
    private String address2;
    private String district;
    private City city;
    private String postalCode;
    private String phone;
    private Timestamp lastUpdate;
}

AddressTests.java

01
02
03
04
05
06
07
08
09
10
11
12
@Test
void testPattern1() {
    Address base = Address.builder().addressId(1).build();
    Address target = Address.builder().addressId(1).build();
    boolean actual = base.equals(target);
    assertThat(actual).isTrue();
    
    base = Address.builder().addressId(1).build();
    target = Address.builder().addressId(5).build();
    actual = base.equals(target);
    assertThat(actual).isFalse();
}

lombokアノテーションではネストされたオブジェクトまで検証する

もし、オブジェクトの中にネストしたオブジェクトが存在する場合、lombokアノテーションでoverrideされたequalsメソッドはネスト先のオブジェクトに対しても値検証の対象にしてくれる。

01
02
03
04
05
06
07
08
09
10
11
@Test
void testPattern1() {
 
    // AssertionFailedError:
    // Expecting:<false> to be equal to:<true> but was not.
    // ネストしたオブジェクトの値が違うため
    Address base = Address.builder().addressId(1).city(City.builder().build()).build();
    Address target = Address.builder().addressId(1).city(City.builder().city("Tokyo").build()).build();
    actual = base.equals(target);
    assertThat(actual).isTrue();
}

というようにとても便利なlombokだが、場合によってはネストされたオブジェクトの比較はしたくない場合がある。
※superClassの比較是非はcallSuperオプションで変えられる。

その場合は自分でメソッドを作る必要があるかもしれない。

自作したフィールド検証用のメソッド

Address.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
 
    private int addressId;
    private String address;
    private String address2;
    private String district;
    private City city;
    private String postalCode;
    private String phone;
    private Timestamp lastUpdate;
 
    public boolean isEqualEntity(Address target) {
        if (!Objects.equals(getAddressId(), target.getAddressId())) { return false; }
        if (!Objects.equals(getAddress(), target.getAddress())) { return false; }
        if (!Objects.equals(getAddress2(), target.getAddress2())) { return false; }
        if (!Objects.equals(getDistrict(), target.getDistrict())) { return false; }
        if (!Objects.equals(getPostalCode(), target.getPostalCode())) { return false; }
        if (!Objects.equals(getPhone(), target.getPhone())) { return false; }
        return Objects.equals(getLastUpdate(), target.getLastUpdate());
    }
}

AddressTests.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Test
void testPattern1() {
    Address base = Address.builder().addressId(1).build();
    Address target = Address.builder().addressId(1).build();
    boolean actual = base.equals(target);
    assertThat(actual).isTrue();
 
    base = Address.builder().addressId(1).build();
    target = Address.builder().addressId(5).build();
    actual = base.equals(target);
    assertThat(actual).isFalse();
 
    // success
    base = Address.builder().addressId(1).city(City.builder().build()).build();
    target = Address.builder().addressId(1).city(City.builder().city("Tokyo").build()).build();
    actual = base.equals(target);
    assertThat(actual).isTrue();
}

自作メソッドなので、本当に値が検証できているのか?ネストされたオブジェクトは検証しないようになっているか?をテストする必要がある。

すると書き方にもよるが、上のテストコードのようにテストコードが観点の数だけ長くなってしまう可能性がある。

フィールド検証用テストを簡潔に書く

ParameterizedTestとReflectionを使って書いてみる

AddressTests.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@ParameterizedTest
@MethodSource("argumentsProvider")
void compareTest(String fieldName, Object baseValue, Object compareValue, boolean result) {
    String className = "com.example.demo.entity.Address";
    String methodName = "isEqualEntity";
 
    new FieldTestUtil(className, methodName, fieldName, baseValue, compareValue, result)
            .assertField();
}
 
static Stream<Arguments> argumentsProvider() {
    return Stream.of(
            Arguments.arguments("city", null, null, true),
            Arguments.arguments("city", City.builder().build(), City.builder().build(), true),
            Arguments.arguments("city", City.builder().build(), City.builder().cityId(4).build(), true),
            Arguments.arguments("addressId", 1, 1, true),
            Arguments.arguments("addressId", 1, 2, false),
            Arguments.arguments("address", null, null, true),
            Arguments.arguments("address", "tokyo", "tokyo", true),
            Arguments.arguments("address", "tokyo", "osaka", false)
    );
}

FieldTestUtil.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class FieldTestUtil {
 
    private final String className;
    private final String methodName;
    private String fieldName;
    private final Object baseFieldValue;
    private final Object compareTargetFieldValue;
    private final boolean result;
 
    public FieldTestUtil(
            String className,
            String methodName,
            String fieldName,
            Object baseFieldValue,
            Object compareTargetFieldValue,
            boolean result
    ) {
        this.className = className;
        this.methodName = methodName;
        this.fieldName = fieldName;
        this.baseFieldValue = baseFieldValue;
        this.compareTargetFieldValue = compareTargetFieldValue;
        this.result = result;
    }
 
    public void assertField() {
 
        try {
            Class<?> targetClass = Class.forName(className);
            Object baseObj = createObject(fieldName, baseFieldValue, targetClass);
            Object compareTarget = createObject(fieldName, compareTargetFieldValue, targetClass);
 
            Method method = targetClass.getMethod(methodName, targetClass);
            boolean actual = (boolean) method.invoke(baseObj, compareTarget);
            assertThat(actual).isEqualTo(result);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            fail("クラス名を確認してください。");
        } catch (InstantiationException |
                IllegalAccessException |
                InvocationTargetException |
                NoSuchMethodException |
                NoSuchFieldException e) {
            e.printStackTrace();
            fail("メソッド名とフィールド名を確認してください。");
        }
    }
 
    private Object createObject(String fieldName, Object fieldValue, Class<?> targetClass)
            throws InstantiationException, IllegalAccessException, InvocationTargetException,
            NoSuchMethodException, NoSuchFieldException
    {
        Object obj = targetClass.getDeclaredConstructor().newInstance();
        Field objField = targetClass.getDeclaredField(fieldName);
        objField.setAccessible(true);
        objField.set(obj, fieldValue);
        return obj;
    }
}

フィールドの検証をリフレクションを使って行い、その設定値をparameterizedすることで、テストしたい項目数分、

Arguments.arguments

を追加するだけでテストができるようになった。

プログラミングカテゴリの最新記事