ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • equals() 메서드의 유효성 단위 테스트
    Study/Java 2023. 9. 23. 11:15

    오늘(9월 22일) 책 Effective Java에서는 equals() 메서드에 관한 부분을 읽었다. 한 번 읽고는 잘 이해가 되지 않는 부분들이 있어서 같은 내용을 두 세번씩은 더 읽었던 것 같다. 아이템 제목은 '아이템 10. equals는 일반 규약을 지켜 재정의하라'였다. 내용에서는 equals() 메서드를 만들 때 지켜야하는 몇 가지 규칙들에 대해 자세히 설명한다. 그 규칙들은 equivalance class(동치류)의 정의로부터 오는 것들인데, 그것들의 단위 테스트를 작성해서 실행해보라는 굵은 글씨를 그냥 지나치기가 좀 그래서, 테스트 코드를 연습할 겸 간단하게 만들어보았다.

    단위 테스트를 실행할 클래스는 오늘 풀었던 BOJ 알고리즘 문제 2887번: 행성 터널에서 만들었던 클래스를 사용했다. 물론 문제를 풀 때 equals() 메서드를 구현해서 풀었던 것은 아니지만, 내가 필요하다고 생각하는 조건을 넣어서 코드를 짜보았다. 아래는 3차원 좌표를 갖고 있는 Planet 클래스와 2개의 Planet 객체와 그 사이의 거리를 필드로 갖는 Tunnel 클래스이다. '아이템 11. equals를 재정의하려거든 hashCode도 재정의하라'의 정신을 따라 hashCode() 메서드도 추가해보았다.

    class Planet {
        private final int x, y, z;
        private int hashCode;
        private Planet(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        public static Planet create(int x, int y, int z) { return new Planet(x, y, z); }
        ...
        ...
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Planet planet = (Planet) o;
            return x == planet.x && y == planet.y && z == planet.z;
        }
        @Override
        public int hashCode() {
            int result = hashCode;
            if (result == 0) {
                result = Integer.hashCode(x);
                result = 31 * result + Integer.hashCode(y);
                result = 31 * result + Integer.hashCode(z);
                hashCode = result;
            }
            return result;
        }
    }
    
    class Tunnel implements Comparable<Tunnel> {
        private final Planet star1, star2;
        private final int distance;
        private Tunnel(Planet star1, Planet star2) {
            if (star1.getX() > star2.getX() ||
                (star1.getX() == star2.getX() && star1.getY() > star2.getY()) ||
                (star1.getX() == star2.getX() && star1.getY() == star2.getY() && star1.getZ() > star2.getZ())) {
                this.star1 = star2;
                this.star2 = star1;
            } else {
                this.star1 = star1;
                this.star2 = star2;
            }
            this.distance = Math.min(Math.abs(star1.getX()- star2.getX()),
                    Math.min(Math.abs(star1.getY() - star2.getY()), Math.abs(star1.getZ() - star2.getZ())));
        }
        public static Tunnel create(Planet star1, Planet star2) {
            return new Tunnel(star1, star2);
        }
        ...
        ...
        @Override
        public int compareTo(Tunnel other) {
            return this.distance - other.distance;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Tunnel tunnel = (Tunnel) o;
            return (Objects.equals(star1, tunnel.star1) && Objects.equals(star2, tunnel.star2));
        }
        @Override
        public int hashCode() {
            return Objects.hash(star1, star2);
        }
    }

    먼저 Planet 클래스의 equals() 메서드가 반사성(reflexivity), 대칭성(symmetry), 추이성(transitivity), 일관성(consistency), null-아님을 확인해야하는데, 책에서는 대칭성, 추이성, 일관성만 확인해도 충분하다고 쓰여있다. 반사성과 null-아님은 equals() 메서드 내부에서 이미 확인이 되기 때문에 그런 것 같다. 나는 IntelliJ를 사용하고 있는데, equals() 메서드를 기본적으로 만들어줄 때 그것에 관한 코드가 포함되어 있다. 아래는 Planet 클래스의 equals() 메서드 테스트 코드이다.

    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    import java.util.Random;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class PlanetTest {
        Random randomNumber = new Random();
        private final int a1 = randomNumber.nextInt(100),
                a2 = randomNumber.nextInt(100),
                a3 = randomNumber.nextInt(100);
    
        @Test
        @DisplayName("check symmetry")
        void testEqualsSymmetry1() {
            // given
            final Planet planet1 = Planet.create(a1, a2, a3);
            final Planet copyOfPlanet1 = Planet.create(planet1.getX(), planet1.getY(), planet1.getZ());
            // when
            final boolean symmetry1 = planet1.equals(copyOfPlanet1);
            // then
            assertTrue(symmetry1);
            // when
            final boolean symmetry2 = copyOfPlanet1.equals(planet1);
            // then
            assertTrue(symmetry2);
        }
    
        @Test
        @DisplayName("check transitivity")
        void testEqualsTransitivity() {
            // given
            final Planet planet1 = Planet.create(a1, a2, a3);
            final Planet planet2 = Planet.create(planet1.getX(), planet1.getY(), planet1.getZ());
            final Planet planet3 = Planet.create(planet2.getX(), planet2.getY(), planet2.getZ());
            // when
            final boolean transitivity12 = planet1.equals(planet2);
            final boolean transitivity23 = planet2.equals(planet3);
            // then
            assertTrue(transitivity12 && transitivity23);
            // when
            final boolean transitivity13 = planet1.equals(planet3);
            // then
            assertTrue(transitivity13);
        }
    
        @Test
        @DisplayName("check consistency")
        void testEqualsConsistency() {
            // given
            final Planet planet1 = Planet.create(a1, a2, a3);
            final Planet planet2 = Planet.create(planet1.getX(), planet1.getY(), planet1.getZ());
            // when
            final boolean consistency1 = planet1.equals(planet2);
            // then
            assertTrue(consistency1);
            // when
            final Planet modifiedPlanet2 = Planet.create(planet2.getX() - 100, planet2.getY(), planet2.getZ());
            final boolean consistency2 = planet1.equals(modifiedPlanet2);
            // then
            assertFalse(consistency2);
        }
    }

    일관성을 확인하는 코드에서 약간 억지스러운 느낌이 있지만, 연습한다는 생각으로 그냥 만들어 보았다. 예제를 고를 때 좀 더 적절한 것을 골랐어야 하나 싶기도 했다. 다음은 Tunnel 클래스의 equals() 메서드 테스트 코드이다.

    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    import java.util.Random;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    class TunnelTest {
        Random randomNumber = new Random();
        private final int a1 = randomNumber.nextInt(100),
                a2 = randomNumber.nextInt(100),
                a3 = randomNumber.nextInt(100);
        private final Planet star1 = Planet.create(a1, a2, a3),
                star2 = Planet.create(a1 + 100, a2 + 100, a3 + 100);
    
        @Test
        @DisplayName("check Symmetry")
        void testEqualsSymmetry() {
            // given
            final Tunnel tunnel1 = Tunnel.create(star1, star2);
            final Tunnel tunnel2 = Tunnel.create(star2, star1);
            // when
            final boolean symmetry1 = tunnel1.equals(tunnel2);
            // then
            assertTrue(symmetry1);
            // when
            final boolean symmetry2 = tunnel2.equals(tunnel1);
            // then
            assertTrue(symmetry2);
        }
    
        @Test
        @DisplayName("check transitivity")
        void testEqualsTransitivity() {
            // given
            final Tunnel tunnel1 = Tunnel.create(star1, star2);
            final Tunnel tunnel2 = Tunnel.create(tunnel1.getStar1(), tunnel1.getStar2());
            final Tunnel tunnel3 = Tunnel.create(tunnel2.getStar2(), tunnel2.getStar1());
            // when
            final boolean transitivity12 = tunnel1.equals(tunnel2);
            final boolean transitivity23 = tunnel2.equals(tunnel3);
            // then
            assertTrue(transitivity12 && transitivity23);
            // when
            final boolean transitivity13 = tunnel1.equals(tunnel3);
            // then
            assertTrue(transitivity13);
        }
    
        @Test
        @DisplayName("check consistency")
        void testEqualsConsistency() {
            // given
            final Tunnel tunnel1 = Tunnel.create(star1, star2);
            final Tunnel tunnel2 = Tunnel.create(tunnel1.getStar2(), tunnel1.getStar1());
            // when
            final boolean consistency1 = tunnel1.equals(tunnel2);
            // then
            assertTrue(consistency1);
            // when
            final Planet star3 = Planet.create(a1 + 200, a2 + 200, a3 + 200);
            final Tunnel modifiedTunnel2 = Tunnel.create(star2, star3);
            final boolean consistency2 = tunnel1.equals(modifiedTunnel2);
            // then
            assertFalse(consistency2);
        }
    }

    어려울 것이 없는 코드지만 생각보다 시간이 많이 걸렸다. 아직 테스트 코드를 어떻게 작성하는 지에 대한 개인적인 기준이 잘 만들어져있지 않기도 하고, 작성하면서 생각해야할 것도 적지 않아서 그런 것 같다. 너무 귀찮아하지 말고, 꾸준히 연습해보자.

    아, 물론 `AutoValue`라고 하는, 위의 코드를 안 써도 되도록 도와주는 프레임 워크가 있긴하다. ㅎ


    참고 링크

    'Study > Java' 카테고리의 다른 글

    템플릿 메서드 패턴: Segment Tree  (0) 2023.10.13

    댓글

Designed by Tistory.