同じオブジェクトのインスタンスが2つある時に、フィールドを含めてインスタンスに同じ値が設定されているかどうかを検証する話。
lombokのアノテーションでequalsをoverride
オブジェクト内にフィールドを一つ一つ検証するようなメソッドを作らなくても、lombokの@Data
か@EqualsAndHashCode
アノテーションを追加することでequalsメソッドがoverrideされ、オブジェクトのフィールドの値まで同じか検証してくれるようになります。lombok便利。
Address.java
@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
@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メソッドはネスト先のオブジェクトに対しても値検証の対象にしてくれる。
@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
@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
@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
@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
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
を追加するだけでテストができるようになった。