🎯 0. 들어가기 전
테스트 시에 검증할 대상과 expected 객체의 값이 같은지 비교하는 일이 많았다.
AssertJ의 usingRecursiveComparison을 알기 전까지는
객체의 getter로 값을 직접 비교하거나, equals & hasCode를 재정의해서 비교하는 경우가 많았다.
주변 크루와 리뷰어에게 AssertJ의 usingRecursiveComparison을 배우고
편하게 테스트 코드를 작성해서 공유하고자 한다!
🎯 1. 테스트 예시
하나의 테스트 예시로 usingRecursive 사용을 해보면서 익혀보자!
장바구니 상품의 영속성을 관리해주는 CartItemDao Test 예시를 들도록 하겠다!
테스트의 when 절은 다음과 같다.
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
memberId와 productId 조건으로 장바구니 상품을 조회하여 도메인 객체로 반환하고 있다.
이렇게 cartItemDao.selectByMemberIdAndProductId가 잘 동작하는지 검증하려면,
결과로 나온 CartItem 객체와
내가 반환되길 기대하는 expectedCartItem 객체의 값이 같은지 비교하면 된다.
CartItem 객체와 연관관계가 있는 객체를 나열해보자.
public class CartItem {
private Long id;
private Quantity quantity;
private final Product product;
private final Member member;
}
public class Product {
private Long id;
private final ProductName productName;
private final ProductPrice productPrice;
private final ProductImageUrl productImageUrl;
}
public class Member {
private final Long id;
private final String email;
private final String password;
private final Cash cash;
}
이렇게 관련된 필드, 즉 값을 검증해야 할 필드들이 엄청 많은 것을 알 수 있다.
이러한 상황에서 then 절에서 기존에 사용했던 방법과 usingRecursiveComparison을 사용한 방법을 비교해보자.
🎯 2. getter / equals & hashCode 재정의 사용
2-1. getter
먼저 getter를 사용한 코드를 간단하게 살펴보자.
// given
CartItem expectedCartItem = CartItemFixtures.SEONGHA_CART_ITEM;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem.getId()).isEqualTo(expectedCartItem.getId());
assertThat(cartItem.getQuantity()).isEqualTo(expectedCartItem.getQuantity());
assertThat(cartItem.getProduct().getId()).isEqualTo(expectedCartItem.getProduct().getId());
// 대충 10개 넘는 assertThat
...
getter를 사용하면 위처럼 코드가 엄청 길어지고 가독성도 좋지 않다.
무엇보다 테스트 검증부 작성 비용이 엄청 많이 든다.
expectedCartItem을 여러 곳에서 사용한다고 하면, 그때마다 10줄 넘는 코드를 직접 작성해야 할 것이다.
2-2. equals & hashCode 재정의
다음으로, equals & hashCode 재정의를 사용해보자.
equals & hashCode 재정의를 해서 값이 같다면 같은 객체로 판단하도록 만들 수 있다.
// given
CartItem expectedCartItem = CartItemFixtures.SEONGHA_CART_ITEM;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).isEqualTo(expectedCartItem);
getter 사용 이후에 이 검증부를 보면 너무나 깔끔해진 것을 알 수 있다.
검증부는 깔끔해졌지만, 내가 equals & hashCode를 재정의하면서 느낀 큰 단점이 있었다.
바로, 테스트만을 위한 코드가 프로덕션 코드에 들어간다는 점이다.
도메인 로직이 equals & hashCode를 재정의해야하는 로직이 있었으면 상관이 없었겠지만,
대부분 테스트의 편의를 위해서 테스트에서만 사용되는 equals & hashCode를 재정의했었다.
이렇듯, getter를 사용하는 것보다는 좋지만 단점이 존재했다.
🎯 3. AssertJ의 usingRecursiveComparison 살펴보기
적절한 대안을 찾지 못해서 equals & hashCode를 사용해서 검증하던 도중에
AssertJ의 usingRecursiveComparison을 발견하게 되었다!
usingRecursiveComparison 코드를 들어가보면 주석으로 다음과 같은 설명을 하고 있다.
첫 번째 박스에서는 usingRecursiveComparison의 기능을 설명하고 있다.
요약하면, '실제 객체와 expected 객체의 필드를 재귀적으로 비교한다' 라는 내용이다.
두 번째 박스에서는 usingRecursiveComparison 사용 시 사용할 수 있는 추가 기능을 나열하고 있다.
이번 글에서는 내가 사용해본 다음과 같은 추가 기능들을 살펴보겠다!
1. 비교 시 필드 무시하기
2. iterable한 비교 검증(리스트 객체 검증)
🎯 4. AssertJ의 usingRecursiveComparison 적용하기
기존 테스트 코드에서 usingRecursiveComparison을 적용해보자.
✅ 4-1. 기본 usingRecursiveComparison
// given
CartItem expectedCartItem = CartItemFixtures.SEONGHA_CART_ITEM;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).usingRecursiveComparison().isEqualTo(expectedCartItem);
사용법은 간단하다.
usingRecursiveComparison()을 체이닝 사이에 선언해주면 된다!
이렇게 하면, CartItem과 관련된 객체의 필드를 모두 재귀적으로 돌면서 값만을 비교하게 된다.
값이 모두 같으면 테스트가 통과하고 하나라도 다르면 테스트가 실패하게 된다.
기본적으로는 이렇게 사용할 수 있다.
이렇게 사용 시 테스트를 위한 equals & hashCode를 없앤다는 점에서 너무 좋게 느껴졌다!
또한 앞에서 말했던 부가 기능들도 사용할 수 있었다.
✅ 4-1. 비교 시 필드 무시하기
보통 ID를 가지는 도메인 객체를 테스트할 때,
데이터 관리나 테스트 격리 문제에 의해 ID를 제외한 다른 값이 같아도,
ID가 초기화되지 않아서 테스트가 실패하는 경우가 있다.
물론 ID를 초기화해주면 되겠지만, 테스트의 목적이 도메인의 ID가 아닌 '값 필드'검증인 경우에는
불필요한 작업일 수 있다.
이때, usingRecursiveComparison의 ignoringFields()를 사용하면 이를 해결할 수 있다.
위의 예시에서 CartItem의 id를 비교에서 제외하고 싶다면 다음과 같이 사용하면 된다.
// given
CartItem expectedCartItem = CartItemFixtures.SEONGHA_CART_ITEM;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(expectedCartItem);
ignoringFields()의 값으로 비교하지 않을 필드 이름을 지정하여 무시하고 비교할 수 있다.
필드뿐만 아니라, ignoringFieldsOfTypes()로 해당 타입을 모두 무시할 수 있다.
// given
CartItem expectedCartItem = CartItemFixtures.SEONGHA_CART_ITEM;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).usingRecursiveComparison()
.ignoringFieldsOfTypes(Long.class)
.isEqualTo(expectedCartItem);
✅ 4-2. iterable한 비교 검증(리스트 객체 검증)
테스트 객체가 List 같은 여러 번 검증을 반복해야 하는 객체일 수 있다.
이때, usingRecursiveFieldByFieldElementComparator()를 사용하면 비교할 수 있다.
// given
List<CartItem> expectedCartItems = CartItemFixtures.SEONGHA_CART_ITEMS;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).usingRecursiveFieldByFieldElementComparator()
.isEqualTo(expectedCartItems);
이렇게 객체를 직접 비교할 수도 있지만, assertJ 메소드로 체이닝할 수 있기 때문에
기존 AssertJ에서 리스트를 검증했던 것처럼
contains, containsExactly, containsAnyOf 같은 메소드를 체이닝해서 사용 할 수도 있다.
// given
List<CartItem> expectedCartItems = CartItemFixtures.SEONGHA_CART_ITEMS;
// when
CartItem cartItem = cartItemDao.selectByMemberIdAndProductId(memberId, productId);
// then
assertThat(cartItem).usingRecursiveFieldByFieldElementComparator()
.containsExactly(CartItemFixtures.SEONGHA_CART_ITEM1, CartItemFixtures.SEONGHA_CART_ITEM2);
이렇게 테스트 시에 AssertJ의 usingRecursiveComparison으로 간단하게 객체 비교를 하는 방법을 알아보았다.
이 글에서 소개한 기능 이외에도 많은 기능들이 존재하니,
필요한 사람은 다음 링크를 참고하여 알아보면 좋을 것 같다!
https://assertj.github.io/doc/#assertj-core-recursive-comparison
'Java' 카테고리의 다른 글
[Java] Collection 복사 - 복사 방법(방어적 복사, 얕은 복사, 깊은 복사) 및 상황별 최적의 복사 방법 (7) | 2023.02.20 |
---|