상황

  • 기존 코드를 리팩토링하는 과정에서 엔티티 객체에 있던 @Data를 @Getter로 변경중이었습니다.

    테스트를 위해 임시로 @Data를 붙여놓고 사용중이었습니다.

    @Getter // 기존 : @Data
    @Builder
    @AllArgsConstructor
    public class LaneInfo {
        private TeamPosition teamPosition;
        private boolean isBottomLane;
        private int myTeamId;
        private int myLaneNumber;
        private int oppositeLaneNumber;
        private int myBottomDuoNumber;
        private int oppositeBottomDuoNumber;
        //...(생략)
    }
    
  • 기존에 잘 동작하던 아래 테스트에서 오류가 발생했습니다.

    //생략
    // matchIndicator가 가진 laneInfo와 given에서 주어진 laneInfo 비교
    assertThat(matchIndicators.get(0)
            .getMetadata()
            .getLaneInfo())
            .isEqualTo(laneInfo);
    

원인 분석

  • 오류 로그는 다음과 같았습니다.
Expected :com.ssafy.matchup_statistics.indicator.entity.match.LaneInfo@1150d471
Actual   :com.ssafy.matchup_statistics.indicator.entity.match.LaneInfo@6393bf8b

해당 테스트코드는 기존에는 잘 동작했고, 각 필드가 하나라도 달라지면 실패하던 테스트코드였기에 원인이 궁금했습니다.

@Data 내부를 확인해본 결과, 기존 코드가 성공했던 이유는 @Data 에서 내부적으로 @EqualsAndHashCode를 통해 equals를 구현해주었기 때문이었습니다.

// 롬복 @Data에 들어가보면 볼 수 있는 Java Doc
/**
 * Generates getters for all fields, a useful toString method, and hashCode and equals implementations that check
 * all non-transient fields. Will also generate setters for all non-final fields, as well as a constructor.
 * Equivalent to {@code @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode}.
 * Complete documentation is found at <a href="https://projectlombok.org/features/Data">the project lombok features page for &#64;Data</a>.
 *
 * @see Getter
 * @see Setter
 * @see RequiredArgsConstructor
 * @see ToString
 * @see EqualsAndHashCode
 * @see lombok.Value
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)

해결

  1. Equals Overriding
@Getter
@Builder
@AllArgsConstructor
@EqualsAndHashCode // 추가
public class LaneInfo {
    private TeamPosition teamPosition;
    private boolean isBottomLane;
    private int myTeamId;
    private int myLaneNumber;
    private int oppositeLaneNumber;
    private int myBottomDuoNumber;
    private int oppositeBottomDuoNumber;

롬복이 제공하는 @EqualsAndHashCode 어노테이션만으로도 간단하게 해결이 가능하지만, Equals를 별도로 구현하지 않고 해결할 수 있는 다른 방법들도 알아보겠습니다.

  1. 각 필드 직접 비교(비추천)
assertThat(matchIndicators.get(0)
        .getMetadata()
        .getLaneInfo()
        .getTeamPosition())
        .isEqualTo(laneInfo.getTeamPosition());

assertThat(matchIndicators.get(0)
        .getMetadata()
        .getLaneInfo()
        .getIsBottomLane())
        .isEqualTo(laneInfo.getIsBottomLane());
//(생략 : 모든 필드 다 비교)

테스트 코드도 길어지고, 필드값 비교 과정에서 human error가 발생할 수 있으므로 비추천합니다.

  1. 재귀적으로 필드값 비교(추천)
assertThat(matchIndicators.get(0)
        .getMetadata()
        .getLaneInfo())
        .usingRecursiveComparison()
        .isEqualTo(laneInfo);

위 코드보다 훨씬 깔끔하게 테스트가 가능합니다. assertJ 짱! 참고로 아래 메서드로 비교하지 않을 필드를 제외할 수 있습니다.

// 무시할 필드값
ignoringFields(String fieldsToIgnore)
// 무시할 정규표현식
ignoreFieldsMatchingRegexes(String regexes)
// 무시할 타입(클래스)
ignoringFieldsOfTypes(Class typesToIgnore)

References

URL게시일자방문일자작성자
https://umanking.github.io/2021/06/11/assertj-field-recursive-comparision/2021.06.11.2024.03.23.CodeNexus
https://assertj.github.io/doc/#assertj-core-recursive-comparison-ignoring-fields2024.02.17.2024.03.23.assertj-core