🌱 들어가기 전
이번 포스팅에서는 간단한 계산기 코드를 리팩토링하고 테스트를 작성하는 과정을 살펴보자.
- 김우근님의 'Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트'를 공부하고 정리한 글입니다. ✏️
지난 포스팅과 이어집니다 !
https://deeper-dev.tistory.com/9
[테스트 코드와 설계] 실패하는 테스트 코드
🌱 들어가기 전이번 포스팅에서는 실패하는 테스트 코드에 대한 분석과 개발자로서 테스트 코드를 짤 때 가져야하는 관점에 대해 알아보자.- 김우근님의 'Java/Spring 테스트를 추가하고 싶은 개
deeper-dev.tistory.com
포스팅 내용에 대한 자세한 코드는 Github에 올려두었습니다.
https://github.com/benjaminuj/test-code-with-architecture/tree/calculator
GitHub - benjaminuj/test-code-with-architecture: 아키텍처에 진화를 주는 테스트 코드에 대해 공부 및 실습
아키텍처에 진화를 주는 테스트 코드에 대해 공부 및 실습합니다. Contribute to benjaminuj/test-code-with-architecture development by creating an account on GitHub.
github.com
간단한 계산기를 구현하여, 테스트 코드 작성 및 리팩토링하는 과정을 살펴보자.
🌱 리팩토링과 테스트 코드 작성
💭 기본 main
public class SampleApplication {
public static void main(String[] args) {
// 입력받기
Scanner scanner = new Scanner(System.in);
System.out.println("Enter two numbers and an operator (e.g 1 + 2): ");
String result = scanner.nextLine();
String[] parts = result.split(" ");
long num1 = Long.parseLong(parts[0]);
long num2 = Long.parseLong(parts[2]);
String operator = parts[1];
// 계산
long answer = switch (operator) {
case "+" -> num1 + num2;
case "-" -> num1 - num2;
case "*" -> num1 * num2;
case "/" -> num1 / num2;
default -> throw new InvalidOperatorException();
};
// 결과 출력
System.out.println(answer);
}
}
이렇게 한 클래스에 동작하는 계산기를 구현했다.
💭 리팩토링 1
계산해주는 기능을 별도의 클래스로 분리할 수 있을 것 같다.
Calculator 클래스
public class Calculator {
public long calculate(long num1, String operator, long num2) {
return switch (operator) {
case "+" -> num1 + num2;
case "-" -> num1 - num2;
case "*" -> num1 * num2;
case "/" -> num1 / num2;
default -> throw new InvalidOperatorException();
};
}
}
참고로 기본 예외말고, Custom Exception을 던지도록 구현하였다.
InvalidOperatorException 클래스
public class InvalidOperatorException extends RuntimeException {
public InvalidOperatorException() {
super("Invalid operator, you need to choose one of (+,-,*,/)");
}
}
그러면 Calculator와 관련한 테스트 코드를 아래와 같이 작성할 수 있다.
CalculatorTest 클래스
public class CalculatorTest {
@Test
public void 덧셈_연산을_할_수_있다() {
//given
// 필요한 값 미리 만들어둠
long num1 = 2;
String operator = "+";
long num2 = 3;
Calculator calculator = new Calculator();
//when
long result = calculator.calculate(num1, operator, num2);
//then
assertEquals(5, result); // junit assertion
// assertThat(result).isEqualTo(5); // assertj assertion
}
@Test
public void 곱셈_연산을_할_수_있다() {
//given
long num1 = 2;
String operator = "*";
long num2 = 3;
Calculator calculator = new Calculator();
//when
long result = calculator.calculate(num1, operator, num2);
//then
assertEquals(6, result);
}
@Test
public void 뺄셈_연산을_할_수_있다() {
//given
long num1 = 2;
String operator = "-";
long num2 = 3;
Calculator calculator = new Calculator();
//when
long result = calculator.calculate(num1, operator, num2);
//then
assertEquals(-1, result);
}
@Test
public void 나눗셈_연산을_할_수_있다() {
//given
long num1 = 6;
String operator = "/";
long num2 = 3;
Calculator calculator = new Calculator();
//when
long result = calculator.calculate(num1, operator, num2);
//then
assertEquals(2, result);
}
@Test
public void 잘못된_연산자가_요청으로_들어올_경우_에러가_난다() {
//given
long num1 = 6;
String operator = "x";
long num2 = 3;
Calculator calculator = new Calculator();
//when
//then
assertThrows(InvalidOperatorException.class, () -> {
calculator.calculate(num1, operator, num2);
});
}
}
참고로, 결과값을 기댓값이랑 비교하는 것은 assertj를 이용하면 더 가독성 좋게 작성할 수 있다.
💭 리팩토링 2
사용자에게 입력받아오는 부분도 분리하면 좋겠다.
CalculationRequestReader 클래스
public class CalculationRequestReader {
public String[] read() {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter two numbers and an operator (e.g 1 + 2): ");
String result = scanner.nextLine();
return result.split(" ");
}
}
CalculationRequestReaderTest 클래스
class CalculationRequestReaderTest {
@Test
public void System_in으로_데이터를_읽어들일_수_있다() {
//given
CalculationRequestReader calculationRequestReader = new CalculationRequestReader();
//when
System.setIn(new ByteArrayInputStream("2 + 3".getBytes())); // 사용자 입력을 넣어주는 것처럼 동작하게 한다
String[] result = calculationRequestReader.read();
//then
assertEquals(2, result[0]);
assertEquals("+", result[1]);
assertEquals(3, result[2]);
}
}
💭 리팩토링 3
그 다음으로 SampleApplicaiton의 아래 코드 부분을 보자. 데이터를 구조화 시킬 수 있겠다.
long num1 = Long.parseLong(parts[0]);
long num2 = Long.parseLong(parts[2]);
String operator = parts[1];
즉 CalculationRequestReader 클래스의 read() 호출 결과를 구조화시키는 것이다. VO를 만들자.
CalulationRequest 클래스
public class CalculationRequest {
private final long num1;
private final long num2;
private final String operator;
public CalculationRequest(String[] parts) {
if (parts.length != 3) {
throw new BadRequestException();
}
String operator = parts[1];
if ((operator.length() != 1) || isInvalidOperator(operator)) {
throw new InvalidOperatorException();
}
this.num1 = Long.parseLong(parts[0]);
this.num2 = Long.parseLong(parts[2]);
this.operator = operator;
}
private static boolean isInvalidOperator(String operator) {
return !operator.equals("+") &&
!operator.equals("-") &&
!operator.equals("*") &&
!operator.equals("/");
}
public long getNum1() {
return num1;
}
public long getNum2() {
return num2;
}
public String getOperator() {
return operator;
}
}
VO는 "VO안의 변수들은 값이 항상 유효하다." 라는 특징이 있다.
그래서 CalulationRequest 생성자에 데이터를 검증하는 로직을 구현했다.
CalculationRequestTest 클래스
class CalculationRequestTest {
@Test
public void 유효한_숫자를_파싱할_수_있다() {
//given
String[] parts = new String[]{"2", "+", "3"};
//when
CalculationRequest calculationRequest = new CalculationRequest(parts);
//then
assertEquals(2, calculationRequest.getNum1());
assertEquals("+", calculationRequest.getOperator());
assertEquals(3, calculationRequest.getNum2());
}
@Test
public void 세자리_숫자가_넘어가는_유효한_숫자를_파싱할_수_있다() {
//given
String[] parts = new String[]{"232", "+", "123"};
//when
CalculationRequest calculationRequest = new CalculationRequest(parts);
//then
assertEquals(232, calculationRequest.getNum1());
assertEquals("+", calculationRequest.getOperator());
assertEquals(123, calculationRequest.getNum2());
}
@Test
public void 유효한_길이의_숫자가_들어오지_않으면_에러를_던진다() {
//given
String[] parts = new String[]{"232", "+"};
//when
//then
assertThrows(BadRequestException.class, () -> {
new CalculationRequest(parts);
});
}
@Test
public void 유효하지_않은_연산자가_들어오면_에러를_던진다() {
//given
String[] parts = new String[]{"232", "x", "2"};
//when
//then
assertThrows(InvalidOperatorException.class, () -> {
new CalculationRequest(parts);
});
}
@Test
public void 유효하지_않은_길이의_연산자가_들어오면_에러를_던진다() {
//given
String[] parts = new String[]{"232", "x-", "2"};
//when
//then
assertThrows(InvalidOperatorException.class, () -> {
new CalculationRequest(parts);
});
}
}
💭 리팩토링 완료 후 main
public class SampleApplication {
public static void main(String[] args) {
CalculationRequest calculationRequest = new CalculationRequestReader().read();
long answer = new Calculator().calculate(
calculationRequest.getNum1(),
calculationRequest.getOperator(),
calculationRequest.getNum2());
System.out.println(answer);
}
}
'Back-End > Test Code' 카테고리의 다른 글
[테스트 코드와 설계] 프로젝트 Test Code 작성과 문제 해결 (0) | 2024.09.04 |
---|---|
[테스트 코드와 설계] 빌더 패턴과 엔티티, 그리고 테스트에 대한 조언 (0) | 2024.09.04 |
[테스트 코드와 설계] 의존성 주입과 의존성 역전을 활용하여 테스트 가능성 높이기 (0) | 2024.09.03 |
[테스트 코드와 설계] 테스트에 대한 개발자의 고민과 이론 및 개념 (1) | 2024.09.03 |
[테스트 코드와 설계] 실패하는 테스트 코드 (0) | 2024.08.29 |