Backend/Java

콘솔 애플리케이션을 만들었을때, 통합 테스트를 해보자. (with. Scanner, System.out.println)

Seyun(Marco) 2023. 12. 31. 00:03
728x90

서론

  • 간단한 사칙 연산(+,-,*,/)이 가능한 애플리케이션을 만들 예정이다. 아래와 같은 출력물로 자바 콘솔에 수식을 입력하고 결과를 출력하게 된다.
    • 참고로 연산자 우선순위는 적용하지 않는다.
1 + 2
결과 : 3
  • 이렇게 실제 입력을 넣엇을때 출력이 잘되는지 테스트 코드를 작성하려고 합니다.
  • 이걸 테스트 코드로 검증하기 위해선 어떻게 해야할지 작성해보겠습니다.

간단한 사칙연산 애플리케이션을 만들기

해당 포스팅은 프로덕션 코드를 잘 작성하기에 목적을 맞춘것이 아니라, 실제 콘솔 애플리케이션에서 어떻게 테스트 코드를 검증해야 하는지에 대해 초점을 맞춘것으로 실제 테스트할 코드는 간단하게 작성하겠습니다.

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class Calculator {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        calculator.calculate();
    }

    public void calculate() {
        Scanner scanner = new Scanner(System.in);
        String formula = scanner.nextLine();

        List<String> operations = Arrays.stream(formula.split(" ")).toList();

        int result = Integer.parseInt(operations.get(0));

        for (int i = 1; i < operations.size(); i += 2) {
            String operator = operations.get(i);
            int number = Integer.parseInt(operations.get(i + 1));

            switch (operator) {
                case "+" -> result += number;
                case "-" -> result -= number;
                case "*" -> result *= number;
                case "/" -> {
                    if (number == 0) {
                        System.out.println("Division by 0!");
                        return;
                    }
                    result /= number;
                }
                default -> throw new IllegalStateException("Unexpected value: " + operator);
            }
        }

        System.out.println("결과: " + result);
    }
}
  • 위 코드는 아주 간단히 요구사항을 충족시킨 코드입니다.
  • 실제 우리가 테스트 할 메서드는 calculate()입니다.

Scanner 테스트 코드 작성하는 법

  • Scanner 테스트 코드를 작성하기 전에 System.in 에 대해서 알아야 합니다.
  • System.in은 자바에서 기본적으로 제공하는 입력 스트림 객체입니다. 이 객체를 통해 키보드나 파일 등 다양한 입력 소스로 부터 데이터를 읽을수 있습니다
  • System.in은 콘솔 화면을 통해 입력된 데이터를 읽는 스트림이기 때문에, 테스트 코드를 실행할 때는 실제 콘솔 화면에 입력된 데이터를 재현할 수 있어야 합니다.
  • System.in은 공유 리소스이기 때문에, 테스트 코드가 종료되면 원래 상태로 복원해야 합니다.
  • 따라서 우리는 아래 코드를 작성하여, 테스트 코드에서 해당 Scanner를 mocking할 예정입니다.
System.setIn(new ByteArrayInputStream("사용자 입력".getBytes()));
  • 여기서 ByteArrayInputStream이란 무엇일지 보면 좋습니다.
  • 바이트 배열을 입력 스트림으로 사용하는 클래스입니다. 이 클래스를 통해 바이트 배열에 저장된 데이터를 읽을 수 있습니다.
  • ByteArrayInputStream은 바이트 배열에 저장된 데이터를 읽는 스트림이기 때문에, 테스트 코드를 실행할 때는 테스트 대상 코드에 입력할 데이터를 바이트 배열에 저장해야 합니다.

System.out.println 테스트 코드 작성 방법

  • System.out.println 테스트 코드를 작성하기 위해서 System.out 을 먼저 알아야 합니다.
  • 해당 메서드는 표준 출력 스트림을 제정의 하는데 사용됩니다.
  • 다른 부분에서 사용되는 공유 정적 리소스로써 테스트가 종료되면 원래 상태로 복원해야 합니다.
  • 해당 메서드를 사용해 재정의를 하면 System.out로 출력한 데이터가 변경될수 있습니다.
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();

System.setOut(new PrintStream(outputStreamCaptor));

실제 테스트 코드를 작성해봅시다.

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @Test
    void calculate() {
        System.setIn(new ByteArrayInputStream("1 + 2".getBytes()));
        ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outputStreamCaptor));

        Calculator calculator = new Calculator();

        calculator.calculate();

        Assertions.assertEquals("결과: 3", outputStreamCaptor.toString().trim());
    }
}
  • 실제 위에 서론에 있는 것처럼 1 + 2를 했을때 결과 : 3이 나오는 테스트 코드를 작성하였습니다.

테스트 케이스를 여러개를 작성해보자.

  • 몇가지 테스트 케이스를 추가해보겠습니다.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

class CalculatorTest {

    @ParameterizedTest
    @CsvSource(value = {"1 + 2, 3", "1 - 2, -1", "1 * 2, 2", "4 / 2, 2", "4 / 0, Division by 0!"})
    void calculate(String input, String expected) {
        System.setIn(new ByteArrayInputStream(input.getBytes()));
        ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outputStreamCaptor));

        Calculator calculator = new Calculator();

        calculator.calculate();

        Assertions.assertEquals("결과: " + expected, outputStreamCaptor.toString().trim());
    }
}
  • ParameterziedTest 를 이용해 몇개의 테스트 케이스를 추가해보았습니다.
  • 실제 해당 테스트코드를 돌려보면 아래와 같이 한개만 통과하고 나머지는 통과하지 않습니다.

  • 왜 이럴까요? 위에서 이야기 했듯이 System.out은 공유 자원이기 떄문에 하나의 테스트가 실행된 이후에 초기화 작업이 필요합니다.
System.setOut(System.out);
  • 위 코드를 통해 기존의 방식으로 초기화가 가능합니다.
  • 그렇다면 맨 아래에 해당 코드를 추가한 뒤에 테스트를 다시 실행해봅시다.
  • 그뒤에 동작시키면 모두다 통과하는 것을 알수 있습니다.

결과

  • 실제로 System.in이나 out을 테스트코드를 작성할 일은 많이 없겠지만, 입출력과 관련된 로직이 어떻게 동작하는지 또한 mock을 이용해서 충분히 가능하며, 차후 학습목적으로 콘솔 애플리케이션을 만들엇을때 통합테스트까지 학습이 가능하니 충분히 학습할 가치가 있으며 이러한 기능들을 이용해 다른 개념들을 학습해 나가면 좋겠습니다.

참고

728x90
728x90