ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 단위 테스트 실습 - 문자열 계산기
    공부/자바 플레이그라운드 with TDD, 클린코드 2022. 2. 13. 17:15

    github

    테스트 코드

    public class StringCalculatorTest {
    
        @ParameterizedTest
        @CsvSource({
                "1 + 2,3",
                "3 * 5,15",
                "10 / 5,2",
                "2 + 3 * 4 / 2,10",
                "5,5"
        })
        @DisplayName("문자열로 들어온 계산식 테스트")
        void execute(String str, int expectedResult) throws Throwable {
            assertThat(new StringCalculator(str).parse().calc()).isEqualTo(expectedResult);
        }
    
        static Stream<Arguments> exceptionTestParameters() {
            return Stream.of(
                    Arguments.of("2 & 3", OperationNotSupportedException.class, "지원하지않는 operation 입니다."),
                    Arguments.of("2 ! 3", OperationNotSupportedException.class, "지원하지않는 operation 입니다."),
                    Arguments.of("2 +", ValidationException.class, "계산식이 잘못되었습니다."),
                    Arguments.of("2 - 3 *", ValidationException.class, "계산식이 잘못되었습니다."),
                    Arguments.of("", Exception.class, "빈 문자열 입니다."),
                    Arguments.of("+", ValidationException.class, "계산식이 순서가 잘못되었습니다."),
                    Arguments.of("1 1 *", ValidationException.class, "계산식이 순서가 잘못되었습니다.")
            );
        }
    
        @ParameterizedTest(name ="{index}: {0}, {2}")
        @MethodSource("exceptionTestParameters")
        @DisplayName("예외 테스트")
        void executeException(String str, Class<Throwable> expectedExceptionClass, String errorMessage) {
            Throwable thr = catchThrowable(() -> new StringCalculator(str).parse().calc());
            assertThat(thr).isInstanceOf(expectedExceptionClass)
                    .hasMessageContaining(errorMessage);
        }
    }

    StringCalculator.java

    public class StringCalculator {
        private final String[] values;
        private final Deque<Integer> numberStack = new LinkedList<>();
        private final Deque<String> operationStack = new LinkedList<>();
    
        public StringCalculator(String str) throws Throwable {
            if (isEmpty(str))
                throw new Exception("빈 문자열 입니다.");
    
            this.values = str.split(" ");
            parse();
        }
    
        private boolean isEmpty(String str) {
            return str == null || str.isEmpty();
        }
    
        private void parse() throws ValidationException {
            for(String v : values){
                discriminateAndPush(v);
            }
            validate();
        }
    
        private void discriminateAndPush(String v) throws ValidationException {
            try {
                numberStack.push(Integer.parseInt(v));
                validateOrderAfterPushingNumber();
            } catch (NumberFormatException nfe) {
                operationStack.push(v);
                validateOrderAfterPushingOperation();
            }
        }
    
        private void validateOrderAfterPushingNumber() throws ValidationException {
            if (numberStack.size() - 1 != operationStack.size())
                throwValidationException();
        }
    
        private void validateOrderAfterPushingOperation() throws ValidationException {
            if (operationStack.size() != numberStack.size())
                throwValidationException();
        }
    
        private void throwValidationException() throws ValidationException {
            throw new ValidationException("계산식이 순서가 잘못되었습니다.");
        }
    
        private void validate() throws ValidationException {
            if (numberStack.size() - 1 != operationStack.size())
                throw new ValidationException("계산식이 잘못되었습니다.");
        }
    
        public int calc() throws OperationNotSupportedException {
            while(!operationStack.isEmpty()) {
                numberStack.offerLast(new Calculator(numberStack.pollLast(), numberStack.pollLast(), operationStack.pollLast()).calc());
            }
    
            return numberStack.pollLast();
        }
    }
    • Deque를 LinkedList로 한 이유는 검색, 삭제하는 로직이 없고 단순히 처음과 끝에 넣고, 빼고만 하기 때문에 선택

    슬랙을 통해 자바지기님에게 몇가지 질문에 대한 답변과 생성자에서 parse를 외부에서 호출하도록 변경하면 어떨지 피드백을 받았다.

    Q. 멤버변수가 클래스 내에 존재하여 바로 접근이 가능하다보니 매개변수로 넘기지 않는점
        - public은 calc 메서드밖에 없다보니 나머지는 전부다 private 메서드 인점..
    A. 메서드를 작은 단위로 분리하다보면 private 메서드가 많아지는 것은 자연스러운 모습
    Q. validation시 조건문이 참이면 예외를 던지는 점, 예외를 던지냐, boolean형으로 던지냐?
    A. 이 부분은 경우에 따라 달라질 것 같아요. 둘 모두 가능한 구현 방법이라 생각해요.

    StringCalculator.java (피드백 반영)

    public class StringCalculator {
        private final String str;
        private final Deque<Integer> numberStack = new LinkedList<>();
        private final Deque<String> operationStack = new LinkedList<>();
    
        public class ParsedStringCalculator {
            private final Deque<Integer> numberStack;
            private final Deque<String> operationStack;
    
            public ParsedStringCalculator(Deque<Integer> numberStack, Deque<String> operationStack) {
                this.numberStack = numberStack;
                this.operationStack = operationStack;
            }
    
            public int calc() throws OperationNotSupportedException {
                while(!operationStack.isEmpty()) {
                    numberStack.offerLast(new Calculator(numberStack.pollLast(), numberStack.pollLast(), operationStack.pollLast()).calc());
                }
    
                return numberStack.pollLast();
            }
        }
    
        /**
         *
         * @param str
         * @throws Exception
         */
        public StringCalculator(String str) throws Exception {
            if (isEmpty(str))
                throw new Exception("빈 문자열 입니다.");
    
            this.str = str;
        }
    
        private boolean isEmpty(String str) {
            return str == null || str.isEmpty();
        }
    
        public ParsedStringCalculator parse() throws ValidationException {
            for(String v : str.split(" ")){
                discriminateAndPush(v);
            }
            validate();
    
            return new ParsedStringCalculator(numberStack, operationStack);
        }
    
        private void discriminateAndPush(String v) throws ValidationException {
            try {
                numberStack.push(Integer.parseInt(v));
                validateOrderAfterPushingNumber();
            } catch (NumberFormatException nfe) {
                operationStack.push(v);
                validateOrderAfterPushingOperation();
            }
        }
    
        private void validateOrderAfterPushingNumber() throws ValidationException {
            if (numberStack.size() - 1 != operationStack.size())
                throwValidationException();
        }
    
        private void validateOrderAfterPushingOperation() throws ValidationException {
            if (operationStack.size() != numberStack.size())
                throwValidationException();
        }
    
        private void throwValidationException() throws ValidationException {
            throw new ValidationException("계산식이 순서가 잘못되었습니다.");
        }
    
        private void validate() throws ValidationException {
            if (numberStack.size() - 1 != operationStack.size())
                throw new ValidationException("계산식이 잘못되었습니다.");
        }
    }
    • innerClass를 생성하여 parse().calc() 체이닝 되도록 변경 (반드시 parse 후 calc가 되도록 하기위함이 목적)

    댓글

Designed by Tistory.