-
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