본문 바로가기

우테코 프리코스

테스트 코드 : NoSuchElementException 오류

package racingcar.view;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import java.io.ByteArrayInputStream;
import org.junit.jupiter.api.Test;

class InputViewTest {
    private InputView inputView;

    @Test
    void inputCarNames() {
        // given
        inputView = new InputView();
        String input = "Tom,Bob,Lisa";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
        System.setIn(inputStream);

        // when
        String[] result = inputView.inputCarNames();

        // then
        assertThat(result).containsExactly("Tom", "Bob", "Lisa");
    }

    @Test
    void inputTryNumber() {
        // given
        inputView = new InputView();
        String input = "5";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
        System.setIn(inputStream);

        // when
        String result = inputView.inputTryNumber();

        // then
        assertThat(result).isEqualTo("5");
    }
}

 

위와 같은 코드를 실행해보면 아래와 같은 오류가 계속해서 발생했다.

 

테스트해야 할 메서드를 따로 하나씩 실행해보면 오류가 나지 않지만 파일 전체를 실행하는 무조건 발생하는 오류였다.

즉, 한개의 테스트만 통과하는 상황이었다.

계속된 검색 결과 작년 우테코 프리코스 하셨던 분이 같은 오류를 경험해 작성해놓은 글을 발견할 수 있었다.

작성자 분께서는 아래와 같이 말씀하셨다.

 

< kjb95 님의 velog 참조 >

 

여기서 나는 의문이 들었다. static 변수로 선언되어서 발생하는 문제는 맞는 거 같은데,

입력 버퍼, 정확히는 입력 스트림에 개행문자가 남아있어서 영향을 줬다는 부분에 대해서는 동의를 하지 못했다.

키보드로 입력시에는 마지막에 엔터를 누르기때문에 개행문자가 포함되지만 테스트 코드의 경우 키보드 입력이 아니기 때문에 개행문자가 없다.

그래도 작성자분 덕분에 static이 문제있는 거 같다는 느낌적인 느낌은 충분히 받았기 때문에 과정을 다시 한번 되짚어 보기로 했다.

 

String input = "Tom,Bob,Lisa";
ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
System.setIn(inputStream);

 

위와 같이 input.getBytes()로 얻은 바이트 배열을 사용하여 ByteArrayInputStream 인스턴스를 생성한다. 이렇게 하면 inputStream이라는 바이트 스트림이 생성된다. 이 스트림은 이제 "Tom,Bob,Lisa" 문자열 데이터를 바이트 단위로 읽을 수 있는 스트림으로 사용될 것이다.

이후 System 클래스의 setIn 메서드를 호출하여 System.in을 inputStream으로 임시로 변경한다. 이것은 프로그램이 실행 중에 사용자로부터의 입력 대신에 inputStream을 사용하도록 하는 것을 의미한다.

 

String[] result = inputView.inputCarNames();

public class InputView {
    public String[] inputCarNames() {
        System.out.println(showInputCarNamesPrompt());
        String inputList = UserInput();
        String[] inputArr = inputList.split(",");
        return inputArr;
    }
    ...
}

 

다음으로 위와같이 inputCarNames()를 실행하면 메서드 내부에서 UserInput()을 호출하게 된다. 

 

public class InputView {
    ...
    private String UserInput() {
        return Console.readLine();
    }
    ...
}

 

UserInput의 호출은 곧 Console.readLine()의 호출이기 때문에 아래와 같이 Console 파일에 들어서게 된다.

 

public class Console {
    private static Scanner scanner;

    private Console() {
    }

    public static String readLine() {
        return getInstance().nextLine();
    }

    public static void close() {
        if (scanner != null) {
            scanner.close();
            scanner = null;
        }
    }

    private static Scanner getInstance() {
        if (scanner == null) {
            scanner = new Scanner(System.in);
        }
        return scanner;
    }
}

 

readLine() 호출로 인해 getInstance()를 실행하게 되고, 테스트를 시작한 초기 상태이기 때문에 scanner는 당연히 객체가 없이 null이다. 따라서 새로운 scanner 객체가 생성되고 아까 System.in을 임시로 inputStream으로 변경했기때문에 scanner 객체는 사용자의 입력을 기다리지 않고 inputStream에서 데이터를 읽어들일 준비 상태가 된다. nextLine()을 호출하면 scanner 객체가 inputStream을 읽고, 개행 문자나 줄 끝을 만날 때까지 읽은 내용을 반환한다. 따라서 "Tom,Bob,Lisa" 를 반환하게 된다.

 

이후 다음 테스트에서 같은 과정을 반복하던 도중에 문제가 발생한다.

 

private static Scanner getInstance() {
        if (scanner == null) {
            scanner = new Scanner(System.in);
        }
        return scanner;
}

 

scanner 객체가 static 이기때문에 첫 테스트에서 생성한 scanner 객체를 공유하여 if 조건을 만족하지 않는다. 그래서 첫 테스트에서 생성했던 scanner 객체를 리턴하게 되고 이 객체에 대해 nextLine()을 실행한 결과 NoSuchElementException 오류가 발생하게 된다. 

그럼 이때 왜 scanner 객체는 아무것도 읽지 못한 걸까? "5" 를 읽는게 맞지 않을까?

 

첫 테스트에서 생성된 scanner 객체가 여전히 이후 테스트에서도 사용된다면 입력 스트림이 갱신되지 못했음을 의미한다. 이후 테스트에서 아무리 System.in을 새로운 inputStream으로 변경해도, 즉 "5" 로 새롭게 변경해도 결국 객체를 새로 선언하지 않으면 아무 의미가 없다. 왜냐하면 첫 객체를 그대로 사용하면서 inputStream 역시 첫 객체 당시의 "Tom,Bob,Lisa"를 읽기 때문이다.

어? 그러면 다시 읽기 때문에 똑같이 "Tom,Bob,Lisa"를 읽어야하는거 아니야? 라고 생각할 수 있다.

하지만 이미 읽었던 데이터를 또 다시 읽는게 아니라, 읽고나면 이미 읽은 위치 다음으로 이동한다. 즉 "Tom,Bob,Lisa"를 읽고 다음 위치로 이동한 상태에서 같은 입력 스트림을 읽으려고 시도했기 때문에 NoSuchElementException 오류가 발생했던 것이다.

 

정리해보자면,

1. 첫 테스트 때에 "Tom,Bob,Lisa" 라는 입력 스트림을 읽는 scanner 객체가 생성되었다.

2. nextLine()을 실행하면서 "Tom,Bob,Lisa" 를 모두 읽고 scanner는 같은 입력 스트림 내에서 다음 데이터를 읽을 준비를 한다.

3. 이후 테스트에서 또다른 입력 스트림이 주어졌지만 기존의 scanner 객체를 그대로 사용하기 때문에 기존의 첫 테스트 때의 scanner 객체가 nextLine()을 실행한다.

4. 기존의 첫 scanner 객체는  "Tom,Bob,Lisa" 를 모두 읽었기 때문에 같은 입력 스트림 내에서 그 다음부터 읽어야하지만 비어있으므로 NoSuchElementException 오류가 발생한다.

 

그렇다면 어떻게 해결해야 할까?

정답은 Console 파일 안에 있었다. close() 메서드를 사용하는 것이다.

 

public static void close() {
        if (scanner != null) {
            scanner.close();
            scanner = null;
        }
}

 

이 메서드를 @AfterEach 로서 선언하여 사용하게 된다면 각 테스트가 끝날 때마다 실행된다.

scanner.close() 를 통해 scanner 객체를 닫으면 scanner 객체가 사용하는 스트림이 해제된다. 그리고 scanner = null 을 통해 이후의 테스트에서 if 문 조건을 만족하도록 하여 새로운 scanner 객체를 생성할 수 있도록 한다.

(갑자기 들은 생각이지만 scanner.close() 없이 scanner == null 만 있어도 되지 않을까 생각해본다. scanner == null 을 통해 새로운 객체를 만들때 새로운 스트림과 연결되므로 자동으로 이전 스트림과는 해제되기 때문이다. 하지만 명시적으로 close 를 해주는건 좋은 습관이고 특정 상황에서는 꼭 필요할 수 있다)

 

아래는 close() 메서드를 적용한 코드다.

 

package racingcar.view;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import camp.nextstep.edu.missionutils.Console;
import java.io.ByteArrayInputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

class InputViewTest {
    private InputView inputView;

    @AfterEach
    void clean() {
        Console.close();
    }

    @Test
    void inputCarNames() {
        // given
        inputView = new InputView();
        String input = "Tom,Bob,Lisa";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
        System.setIn(inputStream);

        // when
        String[] result = inputView.inputCarNames();

        // then
        assertThat(result).containsExactly("Tom", "Bob", "Lisa");
    }

    @Test
    void inputTryNumber() {
        // given
        inputView = new InputView();
        String input = "5";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(input.getBytes());
        System.setIn(inputStream);

        // when
        String result = inputView.inputTryNumber();

        // then
        assertThat(result).isEqualTo("5");
    }
}

 

 

위 코드를 실행해보면 아래와 같이 잘 작동하는 것을 확인할 수 있다.

 

 

나와 같은 오류로 고민하고 고생하신 분들이 이 글을 통해 도움을 받았으면 하는 마음에 이 글을 작성해보았다.

 

 

 

글 작성에 도움을 주신 목록 ->

https://velog.io/@kjb95/%EC%9A%B0%EC%95%84%ED%95%9C-%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-4%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-z2e95enq

'우테코 프리코스' 카테고리의 다른 글

프리코스 3주차 미션  (0) 2023.11.07
2주차 코드에 대한 코드 리뷰  (1) 2023.11.03
프리코스 2주차 미션 (2)  (0) 2023.11.03
프리코스 2주차 미션  (0) 2023.10.28
프리코스 1주차 미션  (0) 2023.10.22