🎁 모든 강의 30% + 무료 강의 선물🎁

워밍업 클럽 3기 BE 클린코드 3주차 발자국

금주 부터 본격적으로 테스트코드 강의 부분 학습을 시작하였다.

강의를 들으며 개인적으로 조금 더 와닿게 느꼈거나 정리하고 싶었던 포인트들만 정리해보았다.

우선 가장 첫번째, 가장 중요한 것은 WHY.귀찮고, 시간도 많이 쏟아야하는, 비용이 발생하는 테스트코드 작성. 왜 테스트를 해야하는가.

(테스트 코드 없는 상황) 사람이 직접 프로덕션으로 나온 프로그램을 테스트 하면?

  • 커버할 수 없는 영역 발생

  • 경험과 감에 의존

  • 늦은 피드백

  • 유지보수 어려움

  • 소프트웨어 신뢰 down

작은 프로젝트면 티가 안날 수도 있지만, 프로젝트가 커지고 기능이 늘어나고 고도화 되면 감당할 수 없게 된다.

그래서 테스트 코드를 통해서 다음을 얻고자 하는것.

  • 빠른 피드백

  • 자동화

  • 안정감

     

TDD

TDD의 장점 : 특정 케이스(해피케이스)만 검증할 가능성을 막아준다.

TDD 하면 처음 부터 메서드 설계가 테스트 하기 좋게 된다.

TDD는 관점의 변화를 준다.

  • Test가 구현부 검증역할을 위한 보조 수단 ---> Test는 구현부와 상호 작용하며 발전

TDD는 클라이언트 관점에서의 피드백을 느낄 수 있다. (메서드/객체를 사용하는 사람 관점에서)

TDD가 만능은 아닌데, 적절한 상황에 사용할 수 있어야 한다. 그럴려면 계속 연습해보고 시도해봐야한다.

 

테스트는 문서다

테스트 코드는 프로덕션 코드라는 공유자산에 대해서 잘 설명해주는 문서 역할도 한다.

 

Repository 테스트 왜 해야하는가?

JPA로 어떤 쿼리 날라가는지 명확히 아는데도 필요한가?

  1. 제대로된 쿼리 날라가는 것 보장하기 위함

  2. 미래에 어떤 형태로 변형 될지 모른다.

     


Day11 미션

 

    @DisplayName("가격과 할인률에 따른 총가격이 정상적으로 계산되어야 한다. 라커 사용 안하는 경우.")
    @ParameterizedTest
    @CsvSource({
        "100, 0, 100",   // price 100, discountRate 0, expectedTotalPrice 100
        "200, 0, 200",   // price 200, discountRate 0, expectedTotalPrice 200
        "300, 0.1, 270",  // price 300, discountRate 10, expectedTotalPrice 270
        "150, 0.5, 75"    // price 150, discountRate 50, expectedTotalPrice 75
    })
    void totalPriceTest(int price, double discountRate, int expectedTotalPrice) {
        // given
        StudyCafeSeatPass pass = StudyCafeSeatPass.of(StudyCafePassType.FIXED, 1, price, discountRate);
        StudyCafePassOrder order = StudyCafePassOrder.of(pass, null);

        // when
        int totalPrice = order.getTotalPrice();

        // then
        assertThat(totalPrice).isEqualTo(expectedTotalPrice);
    }

 

    @DisplayName("가격과 할인률에 따른 총가격이 정상적으로 계산되어야 한다. 라커 사용 하는 경우.")
    @ParameterizedTest
    @CsvSource({
        "100, 0, 10, 110",   // price 100, discountRate 0, lockerPrice 10, expectedTotalPrice 110
        "200, 0, 10, 210",   // price 200, discountRate 0, lockerPrice 10, expectedTotalPrice 210
        "300, 0.1, 10, 280",  // price 300, discountRate 10, lockerPrice 10, expectedTotalPrice 280
        "150, 0.5, 10, 85"    // price 150, discountRate 50, lockerPrice 10, expectedTotalPrice 85
    })
    void totalPriceTestWithLocker(int price, double discountRate, int lockerPrice, int expectedTotalPrice) {
        // given
        StudyCafeSeatPass pass = StudyCafeSeatPass.of(StudyCafePassType.FIXED, 1, price, discountRate);
        StudyCafePassOrder order = StudyCafePassOrder.of(pass, StudyCafeLockerPass.of(StudyCafePassType.FIXED, 1, lockerPrice));

        // when
        int totalPrice = order.getTotalPrice();

        // then
        assertThat(totalPrice).isEqualTo(expectedTotalPrice);
    }

@CsvSource@ParmeterizedTest 를 사용해보았다.

 

InputHandlerTest

@DisplayName("사용자 입력값에 따라 알맞은 이용권이 선택되어야 한다.")
    @Test
    void testGetPassTypeSelectingUserAction_HOURLY() {
        //given
        InputHandler inputHandler = new InputHandler();
        String input = "1\n";
        System.setIn(new ByteArrayInputStream(input.getBytes()));

        // when
        StudyCafePassType passType = inputHandler.getPassTypeSelectingUserAction();

        // then
        assertThat(passType).isEqualTo(StudyCafePassType.HOURLY);
    }

    @DisplayName("사용자 입력값에 따라 알맞은 이용권이 선택되어야 한다.")
    @Test
    void testGetPassTypeSelectingUserAction_WEEKLY() {
        //given
        String input = "2\n";
        System.setIn(new ByteArrayInputStream(input.getBytes()));
        InputHandler inputHandler = new InputHandler();

        // when
        StudyCafePassType passType = inputHandler.getPassTypeSelectingUserAction();

        // then
        assertThat(passType).isEqualTo(StudyCafePassType.WEEKLY);
    }

    @DisplayName("사용자 입력값에 따라 알맞은 이용권이 선택되어야 한다.")
    @Test
    void testGetPassTypeSelectingUserAction_FIXED() {
        //given
        String input = "3\n";
        System.setIn(new ByteArrayInputStream(input.getBytes()));

        InputHandler inputHandler = new InputHandler();

        // when
        StudyCafePassType passType = inputHandler.getPassTypeSelectingUserAction();

        // then
        assertThat(passType).isEqualTo(StudyCafePassType.FIXED);
    }

    @DisplayName("사용자 입력값에 따라 알맞은 이용권이 선택되어야 한다.")
    @Test
    void testGetPassTypeSelectingUserAction_INVALID() {
        //given
        String input = "4\n";
        System.setIn(new ByteArrayInputStream(input.getBytes()));

        InputHandler inputHandler = new InputHandler();

        // when then
        assertThatThrownBy(inputHandler::getPassTypeSelectingUserAction)
            .isInstanceOf(AppException.class);
    }

InputHandler에 static으로 선언되어 있는 Scanner를 공유한다. 위의 네개의 테스트를 각각 실행하면 성공한다.

하지만

  • System.setIn을 사용해 입력 스트림을 변경하고 Scanner가 이 스트림을 사용

  • Scanner는 입력 스트림을 읽을 때, 스트림을 소모. 한 번 사용한 스트림은 소모되므로, 같은 스트림을 여러 테스트에서 공유하면 이후 테스트에서 입력을 읽어들이지 못해 실패

  • 따라서, 각 테스트에서 새로운 입력 스트림을 주입하더라도, 이전 테스트에서 스트림을 사용해버렸기 때문에 이후 테스트들은 스트림을 사용할 수 없게 되는 문제가 발생

     

    하여 순차적으로 실행하면 첫번째로 실행되는 테스트만 통과되고 그 이후 테스트 들에서는 오류 발생으로 실패한다.

     

    여러 다른 방법들을 찾아봤지만, 결국은 이 부분을 테스트 할 수 있게 만들려면 InputHandler가 scanner를 주입받도록 구조를 바꿔야 하는 거 같았다.

    런데 테스트를 위해 구조를 바꾸는 것이 타당한가 생각이 들고 테스트를 위해 프로덕션 코드에 뭔가를 추가하거나 수정하는게 어느 정도 선까지 괜찮은 것인지 기준을 모르겠다. 이 부분을 학습해봐야겠다.

 

 

 

자료, 강의 출처 : https://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C

댓글을 작성해보세요.


채널톡 아이콘