[자바, Java] JUnit 테스트 - Test Fixture

test fixture란??



지난주에 nextstep 1단계 자동차 게임 과정을 전부 끝마친 뒤, 자바지기님의 코드를 분석한 적이 있었다.

근데 테스트 코드에서 각 테스트 클래스마다 상수 형식으로 선언해놓은 모습을 볼 수 있었다.

‘어라?? 이 방법 괜찮은 것 같은데??’ 라는 생각이 들어서, 실제 구현하는 중인 2단계, 3단계에 적용을 해봤다.

그리고 저절로 모든 테스트 클래스들이 그 상수화된 테스트 객체들을 상호 간에 바로바로 사용하면서

테스트 코드가 굉장히 깔끔하게 구현되는 신기한 경험을 하게 되었다.

이러한 구조들을 검색을 통해 Test Fixture 의 일부임을 알 수 있었다.

물론 ‘java-test-fixtures’ 플러그 인을 사용하는 것이 보통이지만,

플러그인을 사용하지 않고 Test Fixture 를 만들면서 프로그램 구현을 했다.

그래서 이번 글에선 공부하면서 알게된 Test Fixture 에 대해 정리해보고자 한다.


1. Test Fixture 는 무엇인가??

  • 여러 테스트에서 같은 Data 를 사용해야 할 때, 코드의 중복을 없애주는 역할을 하는 테스트 전용 데이터이다.
    • 예를 들어, CarTest 와 CarsTest 둘 다 동일하게 사용하는 데이터가 있다면
    • 그 객체를 Test Fixture 로 생성을 해두고, 그 객체를 두 곳에서 가져다 쓰는 형식이다.


2. Test 코드에 공을 들일 필요가 있을까??

  • 테스트 코드는 그 자체로 프로덕션 코드와 동일하게 높은 품질을 유지해야 한다.
  • 만약 테스트 코드의 품질이 낮다면,
    • 여기저기의 테스트가 깨질 것이고,
    • 가독성이 떨어져서 어떤 테스트가 이루어지는지 파악이 어려울 것이고,
    • 테스트 코드의 유지보수는 점점 더 어려워지며,
    • 프로덕션 코드와 테스트 코드에 들어가는 자원이 더욱 많아질 것이다.
    • 그렇다고 테스트를 포기한다??, 지옥의 문이 열릴 것이다.
      • 인간은 신이 아니다.


3. 내가 처음에 구현했던 Test Fixture

  • 내가 setUp() 메서드를 통해 하는 짓이, Test Fixture 를 설정하는 일이라는 것조차 모르던 때의 코드이다.

  • class LottoTicketsTest {
        private List<LottoTicket> ascendingLottoTickets;
        private List<LottoNumber> ascendingLottoNumbers;
        private LottoTickets lottoTickets;
      
        @BeforeEach
        void setUp() {
            ascendingLottoNumbers = IntStream.rangeClosed(1, 6)
                    .mapToObj(LottoNumber::new)
                    .collect(Collectors.toList());
            ascendingLottoTickets = IntStream.range(0, 2)
                    .mapToObj(ticketCount -> new LottoTicket(ascendingLottoNumbers))
                    .collect(Collectors.toList());
            lottoTickets = new LottoTickets(ascendingLottoTickets);
        }
          
        @Test
        @DisplayName("여러장의 로또 생성")
        void create() {
            assertThat(lottoTickets).isEqualTo(new LottoTickets(ascendingLottoTickets));
        }
      
        @Test
        @DisplayName("일치 번호 개수 리스트 반환")
        void numberOfMatches() {
            List<LottoNumber> winningLottoNumbers = Arrays.asList(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(45));
            List<LottoRank> lottoRanks = lottoTickets.lottoRanks(winningLottoNumbers);
            assertThat(lottoRanks).isEqualTo(Arrays.asList(LottoRank.SECOND, LottoRank.SECOND));
        }
    }
    
  • class LottoNumberTest {
        @Test
        @DisplayName("로또 번호 생성")
        void create() {
            LottoNumber lottoNumber = new LottoNumber(8);
            assertThat(lottoNumber).isEqualTo(new LottoNumber(8));
        }
    }
    
  • setUp() 메서드 부분을 보면 엄청나다고 볼 수 있다.

  • Test Fixture 를 설정 해주는 것만으로도 저정도다.

    • 벌써부터 테스트가 하기 싫어진다.
  • 문제는 저런 Test Fixture 를 설정하는 클래스가 한 두개가 아니다.

    • 내가 내 예전 코드를 지금 다시보는데도 얼마나 짜증났을지 감도 안잡힌다.


4. Test Fixture 란 존재를 알고난 후, 내 테스트 코드의 발전 과정

  1. 모든 Test Fixture 를 각 테스트 클래스에 상수로 선언한 형태

    • Test Fixture 를 설정하는 것, 더 나아가 Test 코드도 프로덕션 코드 못지 않게 높은 품질을 유지해야 한다는 사실을 깨달은 이후이다.

    • 처음 자바지기님이 구현한 Test Fixture 의 형태 즉, 각 테스트 클래스의 상수로 생성을 시작했다.

      • public class LottoTicketsTest {
            public static final LottoTickets LOTTO_TICKETS = new LottoTickets(Arrays.asList(LottoTicketTest.LOTTO_TICKET, LottoTicketTest.LOTTO_TICKET));
               
            @Test
            @DisplayName("여러장의 로또 생성")
            void create() {
                assertThat(LOTTO_TICKETS).isNotNull();
            }
               
            @Test
            @DisplayName("일치 번호 개수 리스트 반환")
            void numberOfMatches() {
                List<LottoNumber> winningLottoNumbers = Arrays.asList(LottoNumberTest.ONE, LottoNumberTest.TWO, LottoNumberTest.THREE, LottoNumberTest.FOUR, LottoNumberTest.FIVE, new LottoNumber(45));
                List<LottoRank> lottoRanks = LOTTO_TICKETS.lottoRanks(winningLottoNumbers);
                assertThat(lottoRanks).isEqualTo(Arrays.asList(LottoRank.SECOND, LottoRank.SECOND));
            }
        }
               
        class LottoNumberTest {
            public static final LottoNumber ONE = new LottoNumber(1);
            public static final LottoNumber TWO = new LottoNumber(2);
            public static final LottoNumber THREE = new LottoNumber(3);
            public static final LottoNumber FOUR = new LottoNumber(4);
            public static final LottoNumber FIVE = new LottoNumber(5);
            public static final LottoNumber SIX = new LottoNumber(6);
                   
            @Test
            @DisplayName("로또 번호 생성")
            void create() {
                assertThat(ONE).isNotNull();
            }
        }
        
      • 3번의 예제와 가독성 측면에서 상대조차 되지 않는다.

        • 그냥 아예 setUp() 메서드가 사라졌다.

        • 여러 테스트 클래스들이 상호 간의 Test Fixture 를 공유하면서 코드가 굉장히 깔끔해진 모습을 볼 수 있다.

        • 여기선 Test Fixture 를 설정 하는 부분이 두드러지게 차이가 나지만,

          • 실제로 써보면 테스트를 하는 메서드들도 전부 가독성이 눈에 띄게 좋아진다.


  2. Test Fixture 가 많은 테스트의 경우 Test Fixture 패키지로 따로 추출한다.

    • 사실 이 방법은 위에서 말한 ‘java-test-fixtures 플러그 인’ 을 사용한 방법을, 플러그 인을 사용하지 않고 유사하게 따라한 모양이다.

    • public class LottoTicketsTest {
          public static final LottoTickets LOTTO_TICKETS = new LottoTickets(Arrays.asList(LottoTicketTest.LOTTO_TICKET, LottoTicketTest.LOTTO_TICKET));
               
          @Test
          @DisplayName("여러장의 로또 생성")
          void create() {
              assertThat(LOTTO_TICKETS).isNotNull();
          }
           
          @Test
          @DisplayName("일치 번호 개수 리스트 반환")
          void numberOfMatches() {
              List<LottoNumber> winningLottoNumbers = Arrays.asList(LottoNumberFixture.ONE, LottoNumberFixture.TWO, LottoNumberFixture.THREE, LottoNumberFixture.FOUR, LottoNumberFixture.FIVE, LottoNumberFixture.THIRTY);
              List<LottoRank> lottoRanks = LottoTicketsTest.LOTTO_TICKETS.parseLottoRanks(new WinningLottoNumbers(winningLottoNumbers, LottoNumberFixture.SIX));
              assertThat(lottoRanks).isEqualTo(Arrays.asList(LottoRank.SECOND, LottoRank.SECOND));
          }
      }
           
      public class LottoNumberTest {
          @Test
          @DisplayName("로또 번호 생성")
          void create() {
              assertThat(LottoNumberFixture.ONE).isNotNull();
          }
      }
           
      public class LottoNumberFixture {
          public static final LottoNumber ONE = new LottoNumber(1);
          public static final LottoNumber TWO = new LottoNumber(2);
          public static final LottoNumber THREE = new LottoNumber(3);
          public static final LottoNumber FOUR = new LottoNumber(4);
          public static final LottoNumber FIVE = new LottoNumber(5);
          public static final LottoNumber SIX = new LottoNumber(6);
          public static final LottoNumber THIRTY = new LottoNumber(30);
      }
      
      • LottoNumberTest 의 Test Fixture 만 LottoNumberFixture 로 추출한 모습이다.

        • 패키지도 아예 Fixture 전용으로 따로 만들었다.


5. 마무리 하며…

사실 Test Fixture 플러그 인까지 사용하면서 테스트 구현을 해보고 싶었는데, nextstep 에서 리뷰를 받을 때 뭔가 최대한 외부 API 를 쓰지 않고 순수 객체지향 설계만으로 해결해보자는 오기가 있었던 것 같다. 덕분에 OOP 에 대한 실력이 처음에 비하면 굉장히 올랐다는 것이 체감이 된다. 역시 강사보다 수강생이 고생해야 수강생의 실력이 늘 수 있다는 nextstep, 특히 자바지기님의 철학이 너무 맘에 드는 시간이었다.





© 2021. All rights reserved.

----------Powered by Hydejack----------

Jun's Development Blog