TDD - step02

불순분자들·2022년 11월 3일
0

TDD

목록 보기
2/2

STEP02 요구사항


  • 사용자가 입력한 문자열 값에 따라 사칙연산을 수행할 수 있는 계산기를 구현해야 한다.

  • 입력 문자열의 숫자와 사칙 연산 사이에는 반드시 빈 공백 문자열이 있다고 가정한다.

  • 나눗셈의 경우 결과 값을 정수로 떨어지는 값으로 한정한다.

  • 문자열 계산기는 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다.

1. 테스트를 위한 의존성 추가


    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version> 3.22.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

위 의존성은 "junit", "assertj" 테스트 코드를 사용하기 위한 라이브러리이다.

2. 연산을 담당하는 Controller 클래스 생성


public class CalculatorController {
    // 연산을 위한 메서드
    public int arithmetic(int firstNumber, char operatorSymbol, int secondNumber) {
        if (operatorSymbol == '+') {
            return add(firstNumber, secondNumber);
        }
        if (operatorSymbol == '-') {
            return sub(firstNumber, secondNumber);
        }
        if (operatorSymbol == '/') {
            return div(firstNumber, secondNumber);
        }
        if (operatorSymbol == '*') {
            return mul(firstNumber, secondNumber);
        }
        return 0;
    }

    public int add(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }

    public int sub(int firstNumber, int secondNumber) {
        return firstNumber - secondNumber;
    }

    public int div(int firstNumber, int secondNumber) {
        return firstNumber / secondNumber;
    }

    public int mul(int firstNumber, int secondNumber) {
        return firstNumber * secondNumber;
    }

    public int StringOfCalculate(String str) {
        if (str.equals("0")) {
            return 0;
        }
        if (!str.contains(" ")) {
            return -1;
        }
        String[] value = str.split(" "); // 1 + 2면 value[0]에 1 value[1] + ...
        int result = Integer.parseInt(value[0]);
        for (int i=0; i < value.length-2; i=i+2) {
            result = this.arithmetic(result,
                    value[i+1].charAt(0), Integer.parseInt(value[i+2]));
        } return result;
    }
}
  • str.equals("0") : 이 친구를 이용하여 0이 입력되면 프로그램을 종료하도록 설계하였다.

  • !str.contains(" ") : 공백을 이용하여 문자열을 가르는게 목표이기 때문에 공백을 포함하지 않으면 -1을 리턴시켜줘서 예외를 처리할 것이다.

  • String[] value = str.split(" "); : str을 공백을 통해 갈라서 배열에 저장할 것이다.

  • int result = Integer.parseInt(value[0]); : 먼저 result에 배열의 첫 번째 값을 저장할 것이다. 이유는 이어서 설명하겠다.

for (int i=0; i < value.length-2; i=i+2) {
            result = this.arithmetic(result,
                    value[i+1].charAt(0), Integer.parseInt(value[i+2]));
  • 여기서 첫 번째 값을 저장한 이유가 나오는데, result 대신 value[0 or i]값을 이용한다면 첫 번째 값만 들어가거나, i 가 2씩 증가해 결과값이 달라진다.

  • i+=2 한 이유는 배열에 값을 정확하게 찾기 위함이며, 그 이유로 value.length도 -2 해주어야 index 에러가 나타나지 않는다.

3. 계산기를 표현할 Service 클래스 생성


public class CalculatorService {
    private Scanner scanner; // 스캐너로 입력 받은 값
    private CalculatorController calculate;

    public CalculatorService(Scanner scanner) {
        this.scanner = scanner;
        this.calculate = new CalculatorController();
    }

    public void run() throws IllegalArgumentException {
        String inputArithmeticData = "";
        boolean flag = true;

        do {
            System.out.print("입력 : ");
            inputArithmeticData = scanner.nextLine();

            String target = Optional.ofNullable(inputArithmeticData).orElse("");
            if (target.isBlank()) {
                throw new IllegalArgumentException();
            }
            int result = calculate.StringOfCalculate(inputArithmeticData);

            if (result == -1) {
                System.out.println("올바른 형식으로 입력해주세요.");
            }
            if (result != -1) {
                System.out.println("결과 값은 : " + result);
            }
            if (result == 0) {
                flag = false;
            }
        } while (flag);
    }
}
  • Optional : 객체를 감싸는 것으로, NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.
  • ofNullable() : null인지 아닌지 확신할 수 없는 상황에 사용한다.
  • orElse(T) : ofNullable이 null을 반환한다면 T를 반환한다.

isBlank() VS isEmpty()

  • isBlank() : java 11 이후 추가된 메서드, 문자열이 비어 있거나, 빈 공백으로만 이루어져 있으면, true를 리턴

  • isEmpty() : java 6 이후 추가된 메서드, 문자열의 길이가 0인 경우에, true를 리턴

4. 가시적으로 나타내기 위한 View 클래스 생성


public class CalculatorView {
    private CalculatorService service;

    public CalculatorView() {
        this.service = new CalculatorService(new Scanner(System.in));
    }

    public void viewOfRun() {
        System.out.println("===============문자열 계산기===============");
        System.out.println();
        System.out.println("문자열 연산을 하기위한 값을 / x + y / 형식으로 입력해주세요. * 종료 = 0 * ");

        service.run();
    }
}

5. 쓰레드 안에서 실행하기 위한 main


public class Main {
    public static void main(String[] args) throws IOException {
        CalculatorView calculatorView = new CalculatorView();
        calculatorView.viewOfRun();
    }
}

실행 결과


테스트 코드


@DisplayName("CalculatorMethodTest 클래스")
class CalculatorControllerTest {

    private CalculatorController calculatorController;

    @BeforeEach
    void setup() {
        calculatorController = new CalculatorController();
    }

    @Test
    @DisplayName("연산 테스트")
    void arithmeticTest() {
        //given
        String inputData = "1 + 2 / 3 * 4 - 5";

        //when
        int currentData = calculatorController.StringOfCalculate(inputData);

        //then
        assertThat(currentData).isEqualTo(-1);
    }
}

class CalculatorServiceTest {

    private CalculatorController calculatorController;

    @BeforeEach
    void setUp() {
        calculatorController = new CalculatorController();
    }

    @Test
    @DisplayName("run 메서드의 에러처리 테스트는")
    void runTest() {
        //given
        String data = "";
        String testData = Optional.ofNullable(data).orElse(" ");

        //when, then
        assertThatThrownBy(() -> assertThat(testData).isEqualTo(""))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("null이거나 빈 공백 문자입니다.");
    }
}

리뷰 후 수정한 것

  • 디폴트 생성자를 구우우우지 만든 것
  • 클래스 이름에 Method를 포함하여 클린하지 못한 점
  • 메서드 이름은 '행위'로만
  • 축약 습관
  • 중괄호 생략으로 가독성을 떨어트림
  • 단순한 코드를 구우우우지 메서드로 빼려했던 점
  • 변수를 Null인 상태로 놔두어 NPE를 유발했던 점
  • 비즈니스 로직과 입출력 담당을 나누어야했던 부분( 아직 미흡 )
  • do, while 안티패턴을 사용했던점( while, foreach, stream 방식으로 수정예정 )
  • 생성자를 사용한다면 모든 필드에 대해 값을 입력받을 것, 아니면 외부에서 주입받는 형태
profile
장래희망 : 침대 위 녹아든 치즈

0개의 댓글