diff --git a/README.md b/README.md index 8fe7112..e9f2a20 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,36 @@ -## [NEXTSTEP 플레이그라운드의 미션 진행 과정](https://github.com/next-step/nextstep-docs/blob/master/playground/README.md) +# 문자열 계산기 2번째 구현 --- -## 학습 효과를 높이기 위해 추천하는 미션 진행 방법 +## step2. 기능 요구 맟 구현 순서 ---- -1. 피드백 강의 전까지 미션 진행 -> 피드백 강의 전까지 혼자 힘으로 미션 진행. 미션을 진행하면서 하나의 작업이 끝날 때 마다 add, commit -> 예를 들어 다음 숫자 야구 게임의 경우 0, 1, 2단계까지 구현을 완료한 후 push +1. 연산자(Operator) +- [X] 연산자 기호에 해당하는 연산 담당 객체를 찾아 반환한다. +- [X] 연산자 객체에 두 정수가 전달되면 계산 결과를 반환한다. -![mission baseball](https://raw.githubusercontent.com/next-step/nextstep-docs/master/playground/images/mission_baseball.png) +2. 계산기(Calculator) +- [X] 여러개의 연산자 객체와 정수를 받아 전체 계산 결과를 반환한다. ---- -2. 피드백 앞 단계까지 미션 구현을 완료한 후 피드백 강의를 학습한다. +3. 계산 내용 준비(PrepareCalculationContents) +- [X] 문자열 타입의 수식을 받아, 계산기가 사용할 수 있는 형태로 데이터를 가공하여 반환한다. +- [X] 계산기 객체를 반환한다. + +4. 계산기 프로그램 화면(CalculatorDisplay) +- [X] 안내에 따라 문자열 수식을 입력하면, 계산 결과를 반환한다. 실제 계산은 내부적으로 다른 객체의 도움을 받는다. + +5. 프로그램 실행 장치(Main) +- [] 계산기 프로그램을 실행한다 --- -3. Git 브랜치를 master 또는 main으로 변경한 후 피드백을 반영하기 위한 새로운 브랜치를 생성한 후 처음부터 다시 미션 구현을 도전한다. -``` -git branch -a // 모든 로컬 브랜치 확인 -git checkout master // 기본 브랜치가 master인 경우 -git checkout main // 기본 브랜치가 main인 경우 +## 야구게임에서 받은 피드백 메모 + +- 적절한 객체를 도출하여 객체지향적 구현을 고민해 볼 것 +- 핵심 로직과 UI 로직 분리하여 구현 +- indent depth를 1로 맞출 것(메서드 분리 활용) +- 객체는 상태와 기능을 모두 가질 것 +- 파일 저장시 마지막줄 개행 옵션 체크 +- 의미없는 공백 두지 말 것 +- 지역변수 초기화 시 가급적 inline 사용 +- 불필요한 주석 달지 말 것 -git checkout -b 브랜치이름 -ex) git checkout -b apply-feedback -``` + --- diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/casepractice/calculator/Operator.java b/src/main/java/casepractice/calculator/Operator.java new file mode 100644 index 0000000..f2b17ac --- /dev/null +++ b/src/main/java/casepractice/calculator/Operator.java @@ -0,0 +1,30 @@ +package casepractice.calculator; + +import java.util.Arrays; +import java.util.function.IntBinaryOperator; + +public enum Operator { + SUM("+", Integer::sum), + SUBTRACTION("-", (a, b) -> a - b), + MULTIPLICATION("*", (a, b) -> a * b), + DIVISION("/", (a, b) -> a / b); + + private final String symbol; + private final IntBinaryOperator calculation; + + Operator(String symbol, IntBinaryOperator calculation) { + this.symbol = symbol; + this.calculation = calculation; + } + + static Operator from(String symbol) { + return Arrays.stream(Operator.values()) + .filter(operator -> symbol.equals(operator.symbol)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + + public int calculate(int a, int b) { + return calculation.applyAsInt(a, b); + } +} diff --git a/src/main/java/step1/Calculator.java b/src/main/java/step1/Calculator.java new file mode 100644 index 0000000..49b35ec --- /dev/null +++ b/src/main/java/step1/Calculator.java @@ -0,0 +1,50 @@ +package step1; + +import java.util.List; + +public class Calculator { + + private CalculatorResource calculatorResource; + + public Calculator(String stringCalculatorSentence) { + this.calculatorResource = new CalculatorResource(stringCalculatorSentence); + } + + public int calculate() { + + List numberList = calculatorResource.getNumberArray(); + List operatorList = calculatorResource.getOperatorArray(); + + int operatorListIndex = 0; + int result = 0; + + for (int i = 1; i < numberList.size(); i++) { + + result = calcualteInit(numberList, result, i); + + result = getResult(numberList, operatorList, operatorListIndex, result, i); + operatorListIndex++; + } + return result; + } + + private int calcualteInit(List numberList, int result, int i) { + if (i == 1) { + result = (numberList.get(i - 1)); + } + return result; + } + + private int getResult(List numberList, List operatorList, int operatorListIndex, int result, int i) { + if (operatorList.get(operatorListIndex).equals("+")) { + result = result + (numberList.get(i)); + } else if (operatorList.get(operatorListIndex).equals("-")) { + result = result - (numberList.get(i)); + } else if (operatorList.get(operatorListIndex).equals("*")) { + result = result * (numberList.get(i)); + } else if (operatorList.get(operatorListIndex).equals("/")) { + result = result / (numberList.get(i)); + } + return result; + } +} diff --git a/src/main/java/step1/CalculatorResource.java b/src/main/java/step1/CalculatorResource.java new file mode 100644 index 0000000..414ebca --- /dev/null +++ b/src/main/java/step1/CalculatorResource.java @@ -0,0 +1,64 @@ +package step1; + +import java.util.ArrayList; +import java.util.List; + +public class CalculatorResource { + + private String sentence; + private String[] sentenceArray; + private List numberArray; + private List operatorArray; + + public List getNumberArray() { + return numberArray; + } + + public List getOperatorArray() { + return operatorArray; + } + + public CalculatorResource(String sentence) { + this.sentence = sentence; + splitSentence(); + creatNumberList(); + creatOperatorList(); + } + + private void splitSentence() { + this.sentenceArray = this.sentence.split(" "); + } + + public void creatNumberList() { + this.numberArray = new ArrayList<>(); + + for (String s : sentenceArray) { + char numberCheck = s.charAt(0); + validateNumber(numberArray, s, numberCheck); + } + this.numberArray = numberArray; + } + + private void validateNumber(List numberArray, String s, char numberCheck) { + if (numberCheck > 48 && numberCheck < 58) { + this.numberArray.add(Integer.parseInt(s)); + } + } + + public void creatOperatorList() { + this.operatorArray = new ArrayList<>(); + + for (String s : sentenceArray) { + char operatorCheck = s.charAt(0); + validateOperator(operatorArray, s, operatorCheck); + } + } + + private void validateOperator(List operatorArray, String s, char operatorCheck) { + if (operatorCheck < 48 || operatorCheck > 58) { + this.operatorArray.add(s); + } + } + + +} diff --git a/src/main/java/step1/Main.java b/src/main/java/step1/Main.java new file mode 100644 index 0000000..71d0933 --- /dev/null +++ b/src/main/java/step1/Main.java @@ -0,0 +1,11 @@ +package step1; + +public class Main { + + public static void main(String[] args) { + String stringCalculatorSentence = "1 * 2 - 3 + 4 * 5"; + Calculator calculator = new Calculator(stringCalculatorSentence); + Print print = new Print(calculator.calculate()); + print.printResult(); + } +} diff --git a/src/main/java/step1/Print.java b/src/main/java/step1/Print.java new file mode 100644 index 0000000..4079eec --- /dev/null +++ b/src/main/java/step1/Print.java @@ -0,0 +1,14 @@ +package step1; + +public class Print { + + private int calculateResult; + + public Print(int calculateResult) { + this.calculateResult = calculateResult; + } + + public void printResult() { + System.out.println("calculateResult = " + calculateResult); + } +} diff --git a/src/main/java/step2/CalculatePreparator.java b/src/main/java/step2/CalculatePreparator.java new file mode 100644 index 0000000..d421281 --- /dev/null +++ b/src/main/java/step2/CalculatePreparator.java @@ -0,0 +1,6 @@ +package step2; + +public interface CalculatePreparator { + Calculator create(String stringExpression); +} + diff --git a/src/main/java/step2/Calculator.java b/src/main/java/step2/Calculator.java new file mode 100644 index 0000000..63c5132 --- /dev/null +++ b/src/main/java/step2/Calculator.java @@ -0,0 +1,56 @@ +package step2; + +import java.util.Collections; +import java.util.List; + +public class Calculator { + + private static final int NUMBERS_MIN_SIZE = 2; + private static final int OPERATORS_MIN_SIZE = 1; + private static final int NUMBERS_AND_OPERATORS_INTERVAL = 1; + + private final List numbers; // 숫자만 모아놓은 List + private final List operators; // 연산자만 모아놓은 List + + // 계산기 인스턴스는 생성자를 이용해 계산의 재료가 되는 숫자와 연산자 모음을 필수로 전달 받는다. + public Calculator(List numbers, List operators) { + validateMinSize(numbers, operators); // 연산에 필요한 최소 조건을 검증 + validateFormat(numbers, operators); // 연산자의 갯수가 숫자의 갯수보다 1개 작은지 검증 + this.numbers = Collections.unmodifiableList(numbers); // Read-Only 처리하여 필드로 넘긴다. + this.operators = Collections.unmodifiableList(operators); // Read-Only 처리하여 필드로 넘긴다. + } + + // 계산을 수행할 수 있는 정수와 연산자의 최소 조건(갯수) 검증 + private void validateMinSize(List numbers, List operators) { + if (numbers.size() < NUMBERS_MIN_SIZE) { + // 계산의 대상이 되는 숫자가 2개보다 작을 경우 예외 발생 + throw new IllegalArgumentException(String.format("방정식의 숫자의 수는 %d개 이상이어야 합니다.", NUMBERS_MIN_SIZE)); + } + + if (operators.size() < OPERATORS_MIN_SIZE) { + // 계산에 사용되는 연산자가 없을 경우 예외 발생 + throw new IllegalArgumentException(String.format("방정식의 연산자의 수는 %d개 이상이어야 합니다.", OPERATORS_MIN_SIZE)); + } + } + + // 숫자와 연산자의 갯수 차이를 통해 정상적인 계산이 가능한지 검증 + private void validateFormat(List numbers, List operators) { + int interval = numbers.size() - operators.size(); + if (interval != NUMBERS_AND_OPERATORS_INTERVAL) { + // (숫자 - 연산자)의 수가 1이 아닐 경우 예외 발생 + throw new IllegalArgumentException("방정식의 올바른 형식이 아닙니다."); + } + } + + // Calculator 클래스가 상태로 가지고 있는 숫자와 연산자의 통합 계산을 수행하고, 결과를 반환한다. + public int calculate() { + int result = numbers.get(0); // 첫번째 숫자를 result에 넣어 계산 초기값을 준비한다 + for (int i = 0; i < operators.size(); i++) { // 연산자의 갯수만큼 연산을 반복한다 + Operator operator = operators.get(i);// 순차적으로 연산자 enum 객체를 찾는다 + Integer number = numbers.get(i + 1); // 연산의 대상이 되는 숫자를 number에 셋팅한다 + result = operator.calculate(result, number); // 두 수를 계산하여 중간 계산 결과를 result에 담는다 + } + return result; + } +} + diff --git a/src/main/java/step2/CalculatorDisplay.java b/src/main/java/step2/CalculatorDisplay.java new file mode 100644 index 0000000..7f7c80c --- /dev/null +++ b/src/main/java/step2/CalculatorDisplay.java @@ -0,0 +1,36 @@ +package step2; + +import java.util.Scanner; + +public class CalculatorDisplay { + + private final CalculatePreparator calculatePreparator; + + + // 계산기 재료를 준비해주는 인터페이스를 의존 관계로 받는다. + // 구현체를 변경하는 방식으로 계산기 기능을 다르게 사용할 수 있다. + public CalculatorDisplay(CalculatePreparator calculatePreparator) { + this.calculatePreparator = calculatePreparator; + + } + + // 양식에 맞는 문자열 수식을 넣으면 전체 계산 결과를 반환한다 + private int calculate(String userInputExpression) { + Calculator calculator = calculatePreparator.create(userInputExpression); // 계산 준비 객체에서 계산기 객체를 만든다. + return calculator.calculate(); // 계산기 객체가 계산 결과를 반환한다. + } + + // 테스트 코드 실행용 메서드(scanner 사용 제한) + public int calculateForTest(String userInputExpression) { + Calculator calculator = calculatePreparator.create(userInputExpression); // 계산 준비 객체에서 계산기 객체를 만든다. + return calculator.calculate(); // 계산기 객체가 계산 결과를 반환한다. + } + + public void displayConsole() { + Scanner scanner = new Scanner(System.in); + System.out.println("문자열 계산식을 입력하면 계산 결과를 반환합니다."); + int calculateResult = calculate(scanner.nextLine()); + System.out.println("calculateResult = " + calculateResult); + } +} + diff --git a/src/main/java/step2/Main.java b/src/main/java/step2/Main.java new file mode 100644 index 0000000..f9bc822 --- /dev/null +++ b/src/main/java/step2/Main.java @@ -0,0 +1,11 @@ +package step2; + +public class Main { + + // 프로그램 실행 + public static void main(String[] args) { + CalculatorDisplay calculatorDisplay = new CalculatorDisplay(new StringCalculatePreparator()); + calculatorDisplay.displayConsole(); + } +} + diff --git a/src/main/java/step2/Operator.java b/src/main/java/step2/Operator.java new file mode 100644 index 0000000..5f677aa --- /dev/null +++ b/src/main/java/step2/Operator.java @@ -0,0 +1,37 @@ +package step2; + +import java.util.function.BiFunction; + +import static java.util.Arrays.stream; + +public enum Operator { + + SUM("+", (a, b) -> a + b), + SUBTRACTION("-", (a, b) -> a - b), + MULTIPLICATION("*", (a, b) -> a * b), + DIVISION("/", (a, b) -> a / b); + + // 4개의 enum 객체는 operatorSymbol에 따라 서로 다른 연산 로직을 수행한다. + private final String operatorSymbol; + private final BiFunction biFunction; // 두 개의 Integer 타입을 받아 Integer 타입을 반환 + + // 4개의 enum 객체 생성자 + Operator(String operatorSymbol, BiFunction biFunction) { + this.operatorSymbol = operatorSymbol; + this.biFunction = biFunction; + } + + // enum 객체가 가지고 있는 BiFunction 인터페이스 구현 로직에 따라 계산 결과를 반환한다. + public int calculate(int a, int b) { + return this.biFunction.apply(a, b); + } + + // 문자 형태의 연산자를 operatorSymbol로 가지고 있는 enum 객체를 찾아 반환한다. + static Operator matchOperator(String symbol) { + return stream(Operator.values()) // Operator의 모든 값을 순환한다. + .filter(operator -> symbol.equals(operator.operatorSymbol))// 파라미터로 넘어온 symbol과 enum 객체의 operatorSymbol이 일치하면 통과 + .findFirst() // enum 객체를 찾으면 반환한다. + .orElseThrow(IllegalArgumentException::new); // 일치하는 객체가 없다면 예외 발생 + } +} + diff --git a/src/main/java/step2/StringCalculatePreparator.java b/src/main/java/step2/StringCalculatePreparator.java new file mode 100644 index 0000000..2f8bafd --- /dev/null +++ b/src/main/java/step2/StringCalculatePreparator.java @@ -0,0 +1,43 @@ +package step2; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class StringCalculatePreparator implements CalculatePreparator { + private static final String SEPARATOR = " "; // 문자열 분리의 기준이 되는 상수(공백) + private static final int INTERVAL = 2; // 문자 배열에서 숫자와 연산자의 위치가 구분되는 인덱스 간격 + + // 계산 재료를 준비하는 메서드: 문자열을 수식을 숫자 List와 연산자 List로 분리하고, 계산기 객체를 생성한다. + public Calculator create(String stringExpression) { + List strings = Arrays.asList(stringExpression.split(SEPARATOR));// Arrays.asList()는 Arrays의 private 정적 클래스인 ArrayList를 리턴 + List numbers = createNumbers(strings); // 문자열 수식에서 숫자 부분을 분리한다 + List operators = createOperators(strings); // 문자열 수식에서 연산자 부분을 분리한다. + return new Calculator(numbers, operators); // 숫자와 연산자 List 이용해 방정식 객체를 만든다 + } + + // 문자열의 짝수 위치 문자를 이용해 숫자 List를 만드는 메서드 + private List createNumbers(List strings) { + List evenPositions = new ArrayList<>(); // 문자열 수식에서 짝수에 해당하는 숫자를 분리한다 + for (int i = 0; i < strings.size(); i = i + INTERVAL) { + // INTERVAL 상수를 이용해 문자열 수식의 짝수만 담는다. + evenPositions.add(strings.get(i)); + } + return evenPositions.stream() // 짝수 위치에 해당하는 숫자 List 전체를 순환한다 + .map(Integer::valueOf) // 문자를 숫자로 변환한 stream을 반환 + .collect(Collectors.toList()); // 변환된 숫자들을 List로 모은다 + } + + // 문자열의 홀수 위치 문자를 이용해 연산자 List를 만드는 메서드 + private List createOperators(List strings) { + List oddPositions = new ArrayList<>(); // 홀수 문자열을 담을 List + for (int i = 1; i < strings.size(); i = i + INTERVAL) { + oddPositions.add(strings.get(i)); // 홀수 위치에 해당하는 문자만 담는다. + } + return oddPositions.stream()// 홀수 문자를 순환한다. + .map(Operator::matchOperator) // matchOperator를 참조하여 연산자기호가 일치하는 Enum 객체를 찾아 반환한다 + .collect(Collectors.toList()); // 반환되는 enum 객체를 List로 담는다 + } +} + diff --git a/src/test/java/step1/CalculatorResourceTest.java b/src/test/java/step1/CalculatorResourceTest.java new file mode 100644 index 0000000..6b1de5f --- /dev/null +++ b/src/test/java/step1/CalculatorResourceTest.java @@ -0,0 +1,48 @@ +package step1; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class CalculatorResourceTest { + + @Test + @DisplayName("빈칸으로 구분된 문자열을 받아 숫자만 반환하는 테스트") + void creatNumberListTest() throws Exception { + + // given + String sentence = "51 + 2 + 100"; + CalculatorResource calculatorResource = new CalculatorResource(sentence); + + // when + List numberList = calculatorResource.getNumberArray(); + + // then + List expectList = new ArrayList(Arrays.asList(51, 2, 100)); + Assertions.assertThat(numberList).isEqualTo(expectList); + + } + + @Test + @DisplayName("빈칸으로 구분된 문자열을 받아 연산자만 반환하는 테스트") + void creatOperatorList() { + + // given + String sentence = "1 + 2 - 3 * 100 / 5"; + CalculatorResource calculatorResource = new CalculatorResource(sentence); + + // when + List operatorList = calculatorResource.getOperatorArray(); + + // then + List expectList = new ArrayList(Arrays.asList("+", "-" , "*" , "/")); + Assertions.assertThat(operatorList).isEqualTo(expectList); + } +} diff --git a/src/test/java/step1/CalculatorTest.java b/src/test/java/step1/CalculatorTest.java new file mode 100644 index 0000000..f759c21 --- /dev/null +++ b/src/test/java/step1/CalculatorTest.java @@ -0,0 +1,29 @@ +package step1; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculatorTest { + + @Test + @DisplayName("숫자 리스트와 연산자 리스트로 계산을 실행하는 테스트") + void calculateTest () throws Exception{ + + // given + String sentence = "1 + 2 + 3 - 6 * 100 + 5 / 5"; + + // when + Calculator calculator = new Calculator(sentence); + int result = calculator.calculate(); + + // then + Assertions.assertThat(result).isEqualTo(1); + } +} diff --git a/src/test/java/step2/CalculatorDisplayTest.java b/src/test/java/step2/CalculatorDisplayTest.java new file mode 100644 index 0000000..0b05518 --- /dev/null +++ b/src/test/java/step2/CalculatorDisplayTest.java @@ -0,0 +1,37 @@ +package step2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("CalculatorDisplay 클래스 동작 시나리오 테스트") +class CalculatorDisplayTest { + + @Nested + @DisplayName("Describe: calculate 메소드는") + class Describe_calculate { + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // 계산기 준비 장치를 의존 관계로 주입받은 CalculatorDisplay 객체를 반환한다. + private int subject(String stringExpression) { + CalculatorDisplay calculatorDisplay = new CalculatorDisplay(new StringCalculatePreparator()); + return calculatorDisplay.calculateForTest(stringExpression); + } + + @Nested + @DisplayName("Context: 문자열 방정식이 주어진다면") + class Context_with_input { + @Test + @DisplayName("It: 계산 결과를 반환한다") + void it_returns_a_result() { + assertThat(subject("2 + 3 * 4 / 2")).isEqualTo(10); + } + } + + } +} + diff --git a/src/test/java/step2/CalculatorTest.java b/src/test/java/step2/CalculatorTest.java new file mode 100644 index 0000000..a66dbcf --- /dev/null +++ b/src/test/java/step2/CalculatorTest.java @@ -0,0 +1,104 @@ +package step2; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +// BDD 테스트 코드 작성 패턴 +// Describe : 테스트 대상을 명사로 작성 +// Context : ~인 경우, ~ 할 때, 만약 ~ 한다면과 같이 상황 또는 조건을 기술한다 +// It : 위에서 명사로 작성한 테스트 대상의 행동을 ~이다, ~한다, ~를 갖는다 등으로 작성한다 +@DisplayName("Calculator 클래스 동작 시나리오 테스트") +class CalculatorTest { + + @Nested + @DisplayName("Describe: Calculator 생성자는") + class Describe_constructor { + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // Calculator 생성자는 숫자와 연산자 모음을 받아, 상태로 가지고 있는 인스턴스를 만든다. + private Calculator subject(List numbers, List operators) { + return new Calculator(numbers, operators); + } + + @Nested + @DisplayName("Context: 최소 두 개의 숫자와 하나의 연산자가 넘어올 경우") + class Context_two_number_and_one_operator { + @Test + @DisplayName("It: 계산기 객체를 반환한다.") + void it_returns_a_equation() { + assertNotNull(subject(Arrays.asList(1, 2), Arrays.asList(Operator.SUM))); + assertThat(subject(Arrays.asList(1, 2), Arrays.asList(Operator.SUM))).isInstanceOf(Calculator.class); + } + } + + @Nested + @DisplayName("Context: 한 개의 숫자와 한 개의 연산자만 넘어올 경우") + class Context_one_number { + @Test + @DisplayName("It: 예외가 발생한다") + void it_throws_exception() { + assertThrows(IllegalArgumentException.class, () -> { + subject(Collections.singletonList(1), Collections.singletonList(Operator.SUM)); + }); + } + } + + @Nested + @DisplayName("Context: 연산자가 넘어오지 않을 경우") + class Context_empty_operator { + @Test + @DisplayName("It: 예외가 발생한다") + void it_throws_exception() { + assertThrows(IllegalArgumentException.class, () -> { + subject(Arrays.asList(1, 2), Collections.emptyList()); + }); + } + } + + @Nested + @DisplayName("Context: 숫자 모음과 연산자 모음의 갯수 차이가 1이 아닐 경우") + class Context_wrong_format { + @Test + @DisplayName("It: 예외가 발생한다") + void it_throws_exception() { + assertThrows(IllegalArgumentException.class, () -> { + subject(Arrays.asList(1, 2, 3), Collections.singletonList(Operator.SUM)); + }); + } + } + } + + @Nested + @DisplayName("Describe: calculate 메소드는") + class Describe_calculate { + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // Calculator 인스턴스의 calculate()를 실행한다. + private int subject(List numbers, List operators) { + Calculator calculator = new Calculator(numbers, operators); + return calculator.calculate(); + } + + @Nested + @DisplayName("Context: 정상적인 숫자 List와 연산자 List가 필드에 있을 경우") + class Context_with_numbers_and_operators { + + @Test + @DisplayName("It: calculator를 이용해 전체 계산 결과를 반환한다") + void it_returns_a_result() { + int calculateResult = subject(Arrays.asList(1, 2, 3), Arrays.asList(Operator.SUM, Operator.DIVISION)); + assertThat(calculateResult).isEqualTo(1); + } + } + } +} + + diff --git a/src/test/java/step2/OperatorTest.java b/src/test/java/step2/OperatorTest.java new file mode 100644 index 0000000..b44e021 --- /dev/null +++ b/src/test/java/step2/OperatorTest.java @@ -0,0 +1,156 @@ +package step2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// BDD 테스트 코드 작성 패턴 +// Describe : 테스트 대상을 명사로 작성 +// Context : ~인 경우, ~ 할 때, 만약 ~ 한다면과 같이 상황 또는 조건을 기술한다 +// It : 위에서 명사로 작성한 테스트 대상의 행동을 ~이다, ~한다, ~를 갖는다 등으로 작성한다 +@DisplayName("Operator 클래스 동작 시나리오 테스트") +class OperatorTest { + + @Nested // 테스트 클래스의 하위 구조로 선언한다 + @DisplayName("Describe: matchOperator 메소드는") + class Describe_matchOperator { + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // 문자 타입의 operatorSymbol을 받아, 일치하는 enum 객체를 반환한다. + Operator subject(String symbol) { + return Operator.matchOperator(symbol); + } + + @Nested + @DisplayName("Context: + 문자열이 파라미터로 넘어올 경우") + class Context_with_add_symbol { + + @Test + @DisplayName("It: SUM을 반환한다") + void it_returns_a_sum() { + assertThat(subject("+")).isEqualTo(Operator.SUM); + } + } + + @Nested + @DisplayName("Context: - 문자열이 파라미터로 넘어올 경우") + class Context_with_subtraction_symbol { + @Test + @DisplayName("It: SUBTRACTION을 반환한다") + void it_returns_a_subtraction() { + assertThat(subject("-")).isEqualTo(Operator.SUBTRACTION); + } + } + + @Nested + @DisplayName("Context: * 문자열이 파라미터로 넘어올 경우") + class Context_with_multiplication_symbol { + @Test + @DisplayName("It: MULTIPLICATION을 반환한다") + void it_returns_a_multiplication() { + assertThat(subject("*")).isEqualTo(Operator.MULTIPLICATION); + } + } + + @Nested + @DisplayName("Context: / 문자열이 파라미터로 넘어올 경우") + class Context_with_division_symbol { + @Test + @DisplayName("It: DIVISION을 반환한다") + void it_returns_a_division() { + assertThat(subject("/")).isEqualTo(Operator.DIVISION); + } + } + + @Nested + @DisplayName("Context: 파라미터로 넘어온 문자와 일치하는 operatorSymbol이 없을 경우") + class Context_with_other_symbol { + @ParameterizedTest // 메서드 파라미터에 여러개의 인수를 전달하는 방식으로 테스트 진행 + @ValueSource(strings = {",", ":"}) // 메서드 실행 당 파라미터로 인수(argument)를 하나씩 전달 + @DisplayName("It: 예외가 발생한다.") + void it_throws_exception(String symbol) { + assertThrows(IllegalArgumentException.class, () -> { + subject(symbol); + }); + } + } + } + + @Nested + @DisplayName("Describe: calculate 메소드는") + class Describe_calculate { + private int a, b; + + // 임의의 테스트 값 세팅 + @BeforeEach + void prepareNumbers() { + a = 10; + b = 5; + } + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // Enum 객체와 두 수를 받아 계산 결과를 반환한다. + int subject(Operator operator, int a, int b) { + return operator.calculate(a, b); + } + + @Nested + @DisplayName("Context: 덧셈을 담당하는 SUM이 두 정수 a와 b를 받을 경우") + class Context_with_add_and_numbers { + @Test + @DisplayName("It: a+b의 결과를 반환한다") + void it_returns_a_sum() { + assertThat(subject(Operator.SUM, a, b)).isEqualTo(15); + } + } + + @Nested + @DisplayName("Context: 뺄셈을 담당하는 SUBTRACTION이 두 정수 a와 b를 받을 경우") + class Context_with_subtract_and_numbers { + @Test + @DisplayName("It: a-b의 결과를 반환한다") + void it_returns_a_subtraction() { + assertThat(subject(Operator.SUBTRACTION, a, b)).isEqualTo(5); + } + } + + @Nested + @DisplayName("Context: 곱셈을 담당하는 SUBTRACTION이 두 정수 a와 b를 받을 경우") + class Context_with_multiply_and_numbers { + @Test + @DisplayName("It: a*b의 결과를 반환한다") + void it_returns_a_multiplication() { + assertThat(subject(Operator.MULTIPLICATION, a, b)).isEqualTo(50); + } + } + + @Nested + @DisplayName("Context: 나눗셈을 담당하는 DIVISION이 두 정수 a와 b를 받을 경우") + class Context_with_divide_and_numbers { + @Test + @DisplayName("It: a/b의 결과를 반환하고, 나머지 있을 경우 생략한다.") + void it_returns_a_division() { + assertThat(subject(Operator.DIVISION, a, b)).isEqualTo(2); + } + } + + @Nested + @DisplayName("Context: 나눗셈을 담당하는 DIVISION이 받은 수에 0이 있을 경우") + class Context_with_divide_and_numbers_with_zero { + @Test + @DisplayName("It: ArithmeticException 예외가 발생한다") + void it_throws_exception() { + assertThrows(ArithmeticException.class, () -> { + subject(Operator.DIVISION, 0, 0); + }); + } + } + } +} + diff --git a/src/test/java/step2/PrepareCalculationContentsTest.java b/src/test/java/step2/PrepareCalculationContentsTest.java new file mode 100644 index 0000000..d77c256 --- /dev/null +++ b/src/test/java/step2/PrepareCalculationContentsTest.java @@ -0,0 +1,47 @@ +package step2; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("PrepareCalculationContentsTest 클래스 동작 시나리오 테스트") +class PrepareCalculationContentsTest { + + @Nested // 테스트 클래스의 하위 구조로 선언한다 + @DisplayName("Describe: create 메소드는") + class Describe_constructor { + + // subject를 이용해 테스트 대상이 되는 메서드를 분리한다. + // create()는 문자열 식을 숫자와 연산자로 가공하고, 전체 계산을 담당하는 Calculator 객체를 반환한다. + private Calculator subject(String stringExpression) { + return new StringCalculatePreparator().create(stringExpression); + } + + @Nested // 테스트 클래스의 하위 구조로 선언한다 + @DisplayName("Context: 규격에 맞는 방정식 문자열이 주어진다면") + class Context_right_input { + @Test + @DisplayName("It: 계산기 객체를 반환한다") + void it_returns_a_equation() { + assertNotNull(subject("1 + 2 / 3 + 5")); + Assertions.assertThat(subject("1 + 2 / 3 + 5")).isInstanceOf(Calculator.class); + } + } + + @Nested + @DisplayName("Context: 규격에 맞지 않는 방정식 문자열이 주어진다면") + class Context_wrong_input { + @Test + @DisplayName("It: 예외가 발생한다") + void it_throws_exception() { + assertThrows(IllegalArgumentException.class, () -> { + subject(" 1 ! 2 - 3"); + }); + } + } + } +} + diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java deleted file mode 100644 index 43e47d9..0000000 --- a/src/test/java/study/StringTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package study; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class StringTest { - @Test - void replace() { - String actual = "abc".replace("b", "d"); - assertThat(actual).isEqualTo("adc"); - } -}