diff --git a/build.gradle b/build.gradle index 87254a3a..4cc70fd9 100644 --- a/build.gradle +++ b/build.gradle @@ -12,8 +12,10 @@ repositories { dependencies { testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation("org.assertj:assertj-core:3.20.2") } test { useJUnitPlatform() } + diff --git a/src/main/java/README.md b/src/main/java/README.md new file mode 100644 index 00000000..80645806 --- /dev/null +++ b/src/main/java/README.md @@ -0,0 +1,65 @@ +## Step1 & Step2 (초간단 계산기 구현 및 테스트) +### 기능 요구사항 +인자 2개를 받아 사칙연산을 할 수 있는 계산기를 구현한다. +사칙연산과 매칭되는 4개의 메서드를 제공한다. +계산된 결과는 정수를 반환한다. + +### 새로운 프로그래밍 요구사항 +메인 메서드는 만들지 않는다. + +### 단위 테스트 +작은 단위로 테스트 할 것. + +### ToDo +- 사칙연산 구현하기 + - 최대한 MVC 구조로 구현하기 +- JUnit 공부하기 +- JUnit 활용하기 + - 단위 테스트 구현하기 + +## Step 3 (문자열 계산기) +### 기능 요구사항 +- 기본적으로 쉼표(,)나 콜론(:)을 구분자로 가지는 경우 + - 기본 구분자로 분리한 각 숫자 합을 반환 + - (예: "" => 0, "1,2" => 3, "1,2:3" => 6) +- 커스텀 구분자 지정 가능, "//"와 "\n" 사이에 위치하는 문자를 사용하는 경우 + -커스텀 구분자로 분리해서 각 숫자 합을 반환 + - (예: "//;\n1;2;3" => 구분자:";", 결과로 6 반환) +- 문자열 계산기에 숫자 이외의 값 전달 경우 RuntimeException 예외 throw +- 문자열 계산기에 음수를 전달하는 경우 RuntimeException 예외 throw + +### 새로운 프로그래밍 요구사항 +- 구현한 문자열 계산기가 예상한대로 작동하는지 JUnit5 활용하여 테스트 자동화 +- 조금 더 복잡한 도메인 대상으로 테스트 작성 경험 + +### 기존 프로그래밍 요구사항 +- 메인 메서드 작성하지 않기 + +### ToDo +- 문자열 계산기 구현하기 + - MVC 구조로 작성하기 +- JUnit5 기반 테스트 작성하기 + - 필요기능? +- AssertJ 기반 테스트 작성하기 + - AssertJ 관련 학습하기 + - AssertJ로 구현하기 + +## Step4 리팩토링 +### 코드 작성 시 주의할 점 +- 클래스 첫 줄에는 개행 넣기 -> 코드 컨벤션 (개행 알아보기) +- 코드에 EOF 발생 -> 무엇인지, 처리법 +- 코드 내부에 사용하는 메서드의 접근 제어자 설정 +- 상수 처리 및 네이밍 +- 메서드 명은 동사로 시작하기 +- static 사용이유 +- test code 네이밍 알아보기 +- test code given-when-then 알아보기 + +### 초간단 계산기 +- 사용자의 입력에 따라서 해당 연산 실행하게 바꾸기 +- 음수일 때 예외 추가 +- 나누는 수가 0일때 예외 처리 +- view에 로직을 처리하지 않게 하기 -> model에서 처리하기 + +### 문자열 계산기 +- sum 메서드 처리 \ No newline at end of file diff --git a/src/main/java/simpleCalculator/controller/CalculatorController.java b/src/main/java/simpleCalculator/controller/CalculatorController.java new file mode 100644 index 00000000..6cfb9cf0 --- /dev/null +++ b/src/main/java/simpleCalculator/controller/CalculatorController.java @@ -0,0 +1,43 @@ +package simpleCalculator.controller; + +import simpleCalculator.model.Calculator; +import simpleCalculator.model.StringCalculator; +import simpleCalculator.view.InputView; +import simpleCalculator.view.OutView; + +public class CalculatorController { + + private InputView inputView; + private OutView outView; + private Calculator calculator; + private StringCalculator stringCalculator; + + public CalculatorController() { + this.inputView = new InputView(); + this.outView = new OutView(); + this.calculator = new Calculator(); + this.stringCalculator = new StringCalculator(); + } + + private void runSimpleCalculator() { + String input = inputView.getSimpleNumber(); + calculator.setNumber(input); + + int resultAdd = calculator.addNumbers(); + int resultSub = calculator.subNumbers(); + int resultDiv = calculator.divideNumbers(); + int resultMul = calculator.multipleNumbers(); + + outView.printResult(resultAdd); + outView.printResult(resultSub); + outView.printResult(resultDiv); + outView.printResult(resultMul); + } + + private void runStringCalculator() { + String input = inputView.getStringNumber(); + int result = stringCalculator.add(input); + + outView.printResult(result); + } +} diff --git a/src/main/java/simpleCalculator/model/Calculator.java b/src/main/java/simpleCalculator/model/Calculator.java new file mode 100644 index 00000000..74f8f00e --- /dev/null +++ b/src/main/java/simpleCalculator/model/Calculator.java @@ -0,0 +1,68 @@ +package simpleCalculator.model; + +public class Calculator { + + private static final int ZERO = 0; + private static final int ONE = 1; + private static final int TWO = 2; + + private int num1; + private int num2; + + private boolean checkHowManyInput(String[] inputList) { + return inputList.length != TWO; + } + + private String[] splitList(String input) { + return input.split(","); + } + + private int convertToInt(String input) { + int number = Integer.parseInt(input); + if (number < ZERO) { + throw new RuntimeException("음수는 입력할 수 없습니다."); + } + return number; + } + + private int[] convertToIntList(String[] inputList) { + try { + int[] numberList = new int[inputList.length]; + for (int i = ZERO; i < inputList.length; i++) { + numberList[i] = convertToInt(inputList[i]); + } + return numberList; + } catch (NumberFormatException e) { + throw new RuntimeException("숫자가 아닌 다른 형식이 포함되어 있습니다."); + } + } + + public void setNumber(String input) { + String[] inputList = splitList(input); + if (checkHowManyInput(inputList)) { + throw new RuntimeException("2개의 숫자를 입력해주세요."); + } + int[] numbers = convertToIntList(inputList); + this.num1 = numbers[ZERO]; + this.num2 = numbers[ONE]; + } + + public int addNumbers() { + return num1 + num2; + } + + public int subNumbers() { + return num1 - num2; + } + + public int divideNumbers() { + if (num2 == 0) { + throw new RuntimeException("0으로 나누기는 불가합니다."); + } + return num1 / num2; + } + + public int multipleNumbers() { + return num1 * num2; + } +} diff --git a/src/main/java/simpleCalculator/model/StringCalculator.java b/src/main/java/simpleCalculator/model/StringCalculator.java new file mode 100644 index 00000000..259dee63 --- /dev/null +++ b/src/main/java/simpleCalculator/model/StringCalculator.java @@ -0,0 +1,59 @@ +package simpleCalculator.model; + +public class StringCalculator { + + private static final String SEPARATOR_DEFAULTS = ",|:"; + private static final int ZERO = 0; + + private boolean checkBlank(String input) { + return input == null || input.isEmpty(); + } + + private String[] splitInput(String input) { + if (input.startsWith("//")) { + return splitByCustom(input); + } + return input.split(SEPARATOR_DEFAULTS); + } + + private String[] splitByCustom(String input) { + String[] separates = input.split("\n", 2); + String custom = separates[ZERO].substring(2); + return separates[1].split(custom); + } + + private int[] listToInt(String[] inputList) { + try { + int[] numberList = new int[inputList.length]; + for (int i = ZERO; i < inputList.length; i++) { + numberList[i] = convertToInt(inputList[i]); + } + return numberList; + } catch (NumberFormatException e) { + throw new RuntimeException("숫자가 아닌 다른 형식이 포함되었습니다."); + } + } + + private int convertToInt(String input) { + int number = Integer.parseInt(input); + if (number < ZERO) { + throw new RuntimeException("음수는 입력할 수 없습니다."); + } + return number; + } + + private int sum(int[] inputList) { + int total = ZERO; + for (int number : inputList) { + total += number; + } + return total; + } + + public int add(String input) { + if (checkBlank(input)) { + return ZERO; + } + return sum(listToInt(splitInput(input))); + } +} diff --git a/src/main/java/simpleCalculator/view/InputView.java b/src/main/java/simpleCalculator/view/InputView.java new file mode 100644 index 00000000..308bef22 --- /dev/null +++ b/src/main/java/simpleCalculator/view/InputView.java @@ -0,0 +1,22 @@ +package simpleCalculator.view; + +import java.util.Scanner; + +public class InputView { + + private final Scanner scanner; + + public InputView() { + this.scanner = new Scanner(System.in); + } + + public String getSimpleNumber() { + System.out.println("계산할 내용을 입력해주세요.(숫자,연산자,숫자 형식)"); + return scanner.nextLine(); + } + + public String getStringNumber() { + System.out.println("기본(, :)이나 커스텀 구분자(;)를 가지는 문자열을 입력해주세요. (예: 1,2 => 3, //;\\n1;2;3 => 6 반환)"); + return scanner.nextLine(); + } +} diff --git a/src/main/java/simpleCalculator/view/OutView.java b/src/main/java/simpleCalculator/view/OutView.java new file mode 100644 index 00000000..ddf7e1ce --- /dev/null +++ b/src/main/java/simpleCalculator/view/OutView.java @@ -0,0 +1,8 @@ +package simpleCalculator.view; + +public class OutView { + + public void printResult(int result) { + System.out.println("계산 결과: " + result); + } +} diff --git a/src/test/java/calculator/CalculatorTest.java b/src/test/java/calculator/CalculatorTest.java new file mode 100644 index 00000000..39890519 --- /dev/null +++ b/src/test/java/calculator/CalculatorTest.java @@ -0,0 +1,78 @@ +package calculator; + +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 simpleCalculator.model.Calculator; + +import static org.junit.jupiter.api.Assertions.*; + +public class CalculatorTest { + + private static final int EXPECT_VALUE = 3; + + private Calculator calculator; + + @BeforeEach + void setCalculator() { + calculator = new Calculator(); + } + + @DisplayName("연산기능 테스트") + @Nested + class functionTest { + + @DisplayName("더하기") + @Test + void testAdd() { + calculator.setNumber("1,2"); + assertEquals(EXPECT_VALUE, calculator.addNumbers()); + } + + @DisplayName("빼기") + @Test + void testSub() { + calculator.setNumber("4,1"); + assertEquals(EXPECT_VALUE, calculator.subNumbers()); + } + + @DisplayName("나누기") + @Test + void testDiv() { + calculator.setNumber("9,3"); + assertEquals(EXPECT_VALUE, calculator.divideNumbers()); + } + + @DisplayName("곱하기") + @Test + void testAMul() { + calculator.setNumber("3,1"); + assertEquals(EXPECT_VALUE, calculator.multipleNumbers()); + } + } + + @DisplayName("예외처리 테스트") + @Nested + class exceptionTest { + + @DisplayName("0으로 나누기를 시도한 경우") + @Test + void testDivideByZero() { + calculator.setNumber("10,0"); + assertThrows(RuntimeException.class, () -> calculator.divideNumbers()); + } + + @DisplayName("입력을 2개 미만으로 시도한 경우") + @Test + void testInputUnderTwo() { + assertThrows(RuntimeException.class, () -> calculator.setNumber("1")); + } + + @DisplayName("입력을 2개 초과로 시도한 경우") + @Test + void testInputOverTwo() { + assertThrows(RuntimeException.class, () -> calculator.setNumber("1,2,3")); + } + } +} \ No newline at end of file diff --git a/src/test/java/stringCalculator/StringCalculatorTestAssertJ.java b/src/test/java/stringCalculator/StringCalculatorTestAssertJ.java new file mode 100644 index 00000000..7c64e67d --- /dev/null +++ b/src/test/java/stringCalculator/StringCalculatorTestAssertJ.java @@ -0,0 +1,98 @@ +package stringCalculator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import simpleCalculator.model.StringCalculator; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class StringCalculatorTestAssertJ { + + private static final int EXPECT_VALUE_ZERO = 0; + private static final int EXPECT_VALUE_SIX = 6; + + @DisplayName("연산기능 테스트") + @Nested + class functionTest { + + @Test + @DisplayName("값이 없거나 빈 경우") + public void nullAndEmpty() { + StringCalculator calculator = new StringCalculator(); + assertThat(EXPECT_VALUE_ZERO).isEqualTo(calculator.add(null)); + assertThat(EXPECT_VALUE_ZERO).isEqualTo(calculator.add("")); + } + + @Test + @DisplayName("기본 구분자를 사용하는 경우") + public void addWithDefault() { + StringCalculator calculator = new StringCalculator(); + assertThat(EXPECT_VALUE_SIX).isEqualTo(calculator.add("1,2,3")); + assertThat(EXPECT_VALUE_SIX).isEqualTo(calculator.add("1,2:3")); + assertThat(EXPECT_VALUE_SIX).isEqualTo(calculator.add("1:2,3")); + assertThat(EXPECT_VALUE_SIX).isEqualTo(calculator.add("1:2:3")); + } + + @Test + @DisplayName("커스텀 구분자를 사용하는 경우") + public void addWithCustom() { + StringCalculator calculator = new StringCalculator(); + assertThat(EXPECT_VALUE_SIX).isEqualTo(calculator.add("//;\n1;2;3")); + } + } + + @DisplayName("예외처리 테스트") + @Nested + public class exceptionTest { + + @DisplayName("숫자가 아닌 다른 값을 입력한 경우") + @Nested + public class isNotNumber { + + @Test + @DisplayName("예외처리 확인") + public void onlyException() { + StringCalculator calculator = new StringCalculator(); + assertThatThrownBy(() -> { + calculator.add("hi,1,2"); + }).isInstanceOf(RuntimeException.class); + } + + @Test + @DisplayName("예외처리와 예외메세지 확인") + public void messageWithException() { + StringCalculator calculator = new StringCalculator(); + assertThatThrownBy(() -> { + calculator.add("hi,1,2"); + }).isInstanceOf(RuntimeException.class) + .hasMessage("숫자가 아닌 다른 형식이 포함되었습니다."); + } + } + + @DisplayName("음수를 입력한 경우") + @Nested + public class isNegative { + + @Test + @DisplayName("예외처리 확인") + public void onlyException() { + StringCalculator calculator = new StringCalculator(); + assertThatThrownBy(() -> { + calculator.add("-1,1,2"); + }).isInstanceOf(RuntimeException.class); + } + + @Test + @DisplayName("예외처리와 예외메세지 확인") + public void messageWithException() { + StringCalculator calculator = new StringCalculator(); + assertThatThrownBy(() -> { + calculator.add("-1,1,2"); + }).isInstanceOf(RuntimeException.class) + .hasMessage("음수는 입력할 수 없습니다."); + } + } + } +} diff --git a/src/test/java/stringCalculator/StringCalculatorTestJUnit5.java b/src/test/java/stringCalculator/StringCalculatorTestJUnit5.java new file mode 100644 index 00000000..ac4bbfd9 --- /dev/null +++ b/src/test/java/stringCalculator/StringCalculatorTestJUnit5.java @@ -0,0 +1,65 @@ +package stringCalculator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import simpleCalculator.model.StringCalculator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StringCalculatorTestJUnit5 { + + private static final int EXPECT_VALUE_ZERO = 0; + private static final int EXPECT_VALUE_SIX = 6; + + @DisplayName("연산기능 테스트") + @Nested + class functionTest { + + @Test + @DisplayName("값이 없거나 비어있는 경우") + public void nullAndEmpty() { + StringCalculator calculator = new StringCalculator(); + assertEquals(EXPECT_VALUE_ZERO, calculator.add(null)); + assertEquals(EXPECT_VALUE_ZERO, calculator.add("")); + } + + @Test + @DisplayName("기본 구분자를 사용하는 경우") + public void addWithDefault() { + StringCalculator calculator = new StringCalculator(); + assertEquals(EXPECT_VALUE_SIX, calculator.add("1,2,3")); + assertEquals(EXPECT_VALUE_SIX, calculator.add("1,2:3")); + assertEquals(EXPECT_VALUE_SIX, calculator.add("1:2,3")); + assertEquals(EXPECT_VALUE_SIX, calculator.add("1:2:3")); + } + + @Test + @DisplayName("커스텀 구분자를 사용하는 경우") + public void addWithCustom() { + StringCalculator calculator = new StringCalculator(); + assertEquals(EXPECT_VALUE_SIX, calculator.add("//;\n1;2;3")); + } + } + + @DisplayName("예외처리 테스트") + @Nested + class exceptionTest { + @Test + @DisplayName("숫자가 아닌 다른 값을 입력한 경우") + public void isNotNumber() { + StringCalculator calculator = new StringCalculator(); + assertThrows(RuntimeException.class, () -> calculator.add("hi,1,2")); + assertThrows(RuntimeException.class, () -> calculator.add("hi")); + } + + @Test + @DisplayName("음수를 입력한 경우") + public void isNegative() { + StringCalculator calculator = new StringCalculator(); + assertThrows(RuntimeException.class, () -> calculator.add("-2,1,2")); + assertThrows(RuntimeException.class, () -> calculator.add("-2")); + } + } +} \ No newline at end of file