워밍업 클럽 3기 BE 클린코드 3주차 발자국
금주 부터 본격적으로 테스트코드 강의 부분 학습을 시작하였다.
강의를 들으며 개인적으로 조금 더 와닿게 느꼈거나 정리하고 싶었던 포인트들만 정리해보았다.
우선 가장 첫번째, 가장 중요한 것은 WHY.귀찮고, 시간도 많이 쏟아야하는, 비용이 발생하는 테스트코드 작성. 왜 테스트를 해야하는가.
(테스트 코드 없는 상황) 사람이 직접 프로덕션으로 나온 프로그램을 테스트 하면?
커버할 수 없는 영역 발생
경험과 감에 의존
늦은 피드백
유지보수 어려움
소프트웨어 신뢰 down
작은 프로젝트면 티가 안날 수도 있지만, 프로젝트가 커지고 기능이 늘어나고 고도화 되면 감당할 수 없게 된다.
그래서 테스트 코드를 통해서 다음을 얻고자 하는것.
빠른 피드백
자동화
안정감
TDD
TDD의 장점 : 특정 케이스(해피케이스)만 검증할 가능성을 막아준다.
TDD 하면 처음 부터 메서드 설계가 테스트 하기 좋게 된다.
TDD는 관점의 변화를 준다.
Test가 구현부 검증역할을 위한 보조 수단 ---> Test는 구현부와 상호 작용하며 발전
TDD는 클라이언트 관점에서의 피드백을 느낄 수 있다. (메서드/객체를 사용하는 사람 관점에서)
TDD가 만능은 아닌데, 적절한 상황에 사용할 수 있어야 한다. 그럴려면 계속 연습해보고 시도해봐야한다.
테스트는 문서다
테스트 코드는 프로덕션 코드라는 공유자산에 대해서 잘 설명해주는 문서 역할도 한다.
Repository 테스트 왜 해야하는가?
JPA로 어떤 쿼리 날라가는지 명확히 아는데도 필요한가?
제대로된 쿼리 날라가는 것 보장하기 위함
미래에 어떤 형태로 변형 될지 모른다.
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를 주입받도록 구조를 바꿔야 하는 거 같았다.
그
런데 테스트를 위해 구조를 바꾸는 것이 타당한가 생각이 들고 테스트를 위해 프로덕션 코드에 뭔가를 추가하거나 수정하는게 어느 정도 선까지 괜찮은 것인지 기준을 모르겠다. 이 부분을 학습해봐야겠다.
댓글을 작성해보세요.