1주차 우테코 과제
사건의 발단
private method를 테스트할 수 없어서 public method를 테스트 하기에는 사용자 입력에 대해서 테스트해야 되는 문제가 있었습니다.
- 평상시 void 타입 메서드를 test code 작성할 생각도 안해봤는데 우테코에서 System.out.println()을 test하는 것을 보고 찾아보기로 했습니다.
시간이 없으신 분들은 private 메소드 테스트를 지양해야 하는 이유부터 읽어보시면 될거 같습니다.
참조 - https://mangkyu.tistory.com/235
Scanner 클래스
사용자의 입력은 Scanner 혹은 BufferedReader를 사용한다고 생각합니다.
Scanner scanner = new Scanner(System.in);
System.out.println(scanner.nextLine());
지정된 입력 스트림 (system.in)에서 스캔한 값(입력값의 byte 배열)을 생성하는 새로운 Scanner를 만들어 주는 생성자로 바이트 코드를 문자로 변환시켜 줍니다.
따라서 System 클래스에 in 값을 세팅해주면 Scanner 클래스에 nextLine()을 실행할 때 셋팅한 값이 출력 되겠다고 생각할 수 있었습니다.
참조 - https://steadyjay.tistory.com/10
System 클래스의 setIn()메서드
System Class에 내용을 보면서 RuntimePermission을 확인해보니 런타임 권한을 위한 클래스로
가능한 모든 RuntimePermission 대상 이름이 나열되어 있으며 각 이름에 대해 권한이 허용하는 내용에 대한 설명과 코드에 권한을 부여할 때 발생할 수 있는 위험에 대한 설명이 아래 링크에 나와 있습니다.
https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimePermission.html
RuntimePermissoion을 setIO로 설정해서 표준 시스템 스트림의 값을 변경할 수 있습니다.
이 권한을 악의적으로 사용하는 공격자가 System.in을 변경하여 사용자 입력을 감시하거나 도용할 수도 있으며,
System.err을 "null" OutputStream으로 설정하여 오류 메시지를 감추려고 할 수 있습니다.
그러면 밑에 set메서드도 봐야겠죠
setIn()말고도 setOut(), setErr() 메서드가 native 키워드로 JNI(Java Native Interface)를 사용해서 네이티브 코드로 구현되었음을 알 수 있습니다.
네이티브 메소드를 구현하는 주된 이유는 아래와 같습니다.
시스템 퍼포먼스를 개선하기 위해서.
기계/메모리 레벨의 통신을 위해서.
이미 존재하는 자바가 아닌 코드를 사용하기 위해서.
출처 - https://velog.io/@paki1019/Java-native-%ED%82%A4%EC%9B%8C%EB%93%9C
Test 해봐야겠죠?
System 클래스에 in 필드에 사용자가 지정한 입력 스트림을 할당하여 원하는 입력값을 기입할 수 있습니다.
사용자는 InputStream 을 상속받는 ByteArrayInputStream 을 생성하여 입력값의 바이트 배열을 스트림에서 읽을 수 있는 버퍼에 담아 System.in에 할당함으로써 원하는 입력값을 테스팅할 수 있게 됩니다.
출처 - https://steadyjay.tistory.com/10
- 테스트가 통과하는 것을 볼 수 있습니다.
System클래스의 setOut()메서드
setIn()메서드와 동일하게 System.setOut() 메서드를 이용해서 표준 스트림을 재할당할 수 있습니다.
- System.out의 스트림을 출력되는 값의 바이트 코드를 받아오도록 할당해볼 수 있게 됐습니다.
표준 스트림을 초기화하고 할당하는 이유
스트림이 ByteArrayOutputStream 으로 할당된 상태에서는 출력값을 확인할 수 없습니다
(System.out은 자동으로 flush되지 않기 때문에 그런듯 하네요).
즉, 버퍼 내에 출력값이 존재하기 때문에 System.out 표준 스트림 (콘솔에 출력되는 스트림)에 출력값이 존재하지 않는 것이죠.
참조 - https://steadyjay.tistory.com/10
테스트가 왜 실패할까요?
- 눈치가 빠르신 분을 금방 아셨을거라고 생각합니다.
- main문에서 연습하시는 분들이 있을까봐 이해하기 쉽게 작성해봤습니다.
- character 배열로 바꿔서 반복문을 돌려보니 마지막에 11 -1 = 10이 나왔습니다.
아스키 코드 10번은 Line Feed(LF)란 의미를 가지며 일반적으로 New Line으로 \n을 말합니다.
- System.out.println(test) ➡️ System.out.print(test) 로 변경하면 true가 나옵니다.
- System.out.println(test)인데 true의 값을 원한다면 captor.toString().trim()을 이용해도 되겠습니다.
다른 방법으로는 디버깅을 활용하면 좋겠죠?
- 더 쉽고 명확하게 알 수 있습니다!
String클래스의 trim()메서드를 이용해서 테스트 코드를 수정하면 성공하는 것을 볼 수 있습니다.!!
저는 테스트 코드를 어떻게 작성했을까요?
시나리오 테스트를 하고싶어서 문자열 배열을 String.join()메서드를 통해서 하나의 문자열로 변경후 byte 배열로 넣어줬습니다.
@DisplayName("사용자 입력 시나리오")
@TestFactory
Collection<DynamicTest> gameRequest() {
// given
command(new String[]{"123","1234", "28a", "104", "288"});
return List.of(
DynamicTest.dynamicTest("3자리가 아닌경우 예외가 발생한다.", () -> {
//when
String input = RequestChecker.gameRequest();
// then
assertThat("123").isEqualTo(input);
System.out.println(input);
}),
DynamicTest.dynamicTest("3자리가 아닌경우 예외가 발생한다.", () -> {
//when //then
assertThatThrownBy(RequestChecker::gameRequest)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("사용자의 입력값은 3자리 수이며 1-9까지의 값만 가능합니다.");
}),
DynamicTest.dynamicTest("문자가 들어온다면 예외가 발생한다.", () -> {
//when //then
assertThatThrownBy(RequestChecker::gameRequest)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("사용자의 입력값은 3자리 수이며 1-9까지의 값만 가능합니다.");
}),
DynamicTest.dynamicTest("숫자 0이 들어온다면 예외가 발생한다.", () -> {
//when //then
assertThatThrownBy(RequestChecker::gameRequest)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("사용자의 입력값은 3자리 수이며 1-9까지의 값만 가능합니다.");
}),
DynamicTest.dynamicTest("중복된 값이 들어온다면 예외가 발생한다.", () -> {
//when //then
assertThatThrownBy(RequestChecker::gameRequest)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("입력값은 중복되면 안됩니다.");
})
);
}
private void command(final String... results) {
byte[] bytes = String.join("\n", results).getBytes();
System.setIn(new ByteArrayInputStream(bytes));
}
재시도 요청 메서드 테스트
class SupportedRetryRequestTest {
@DisplayName("사용자의 재시도 요청은 1,2 숫자만 요청할 수 있다.")
@TestFactory
Collection<DynamicTest> isRetry() {
// given
return List.of(
DynamicTest.dynamicTest("사용자 입력으로 1이 들어오는 경우", () -> {
//given
String userRequest = "1";
//when
SupportedRetryRequest retryRequest = SupportedRetryRequest.isRetry(userRequest);
//then
assertThat(retryRequest.isFlag()).isTrue();
}),
DynamicTest.dynamicTest("사용자 입력으로 2가 들어오는 경우", () -> {
//given
String userRequest = "2";
//when
SupportedRetryRequest retryRequest = SupportedRetryRequest.isRetry(userRequest);
//then
assertThat(retryRequest.isFlag()).isFalse();
})
);
}
@DisplayName("사용자의 재시도 요청을 검증할 수 있다.")
@ParameterizedTest(name ="{0}를 입력시 예외가 발생한다.")
@ValueSource(strings = {"3","baseball","12","우테코","0"})
public void isRetryException(String request) {
// when // then
Assertions.assertThatThrownBy(() -> SupportedRetryRequest.isRetry(request))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("다시 시도하려면 1 종료하려면 2를 입력하세요");
}
}
사용자에게 힌트를 제공하는 System.out 테스트
@DisplayName("Strike 개수와 ball 개수를 통해서 힌트정보를 출력할 수 있다.")
@TestFactory
public Collection<DynamicTest> hintMessage() {
// given
return List.of(
DynamicTest.dynamicTest("입력한 수에 대한 결과를 볼, 스트라이크 개수로 표시", () -> {
//given
int strikeCount = 1;
int ballCount = 1;
String result = String.format(BALL_AND_STRIKE_FORMAT, ballCount, strikeCount);
init();
//when
Computer.hintMessage(strikeCount,ballCount);
//then
printOutput();
assertThat(getOutput().trim()).isEqualTo(result);
}),
DynamicTest.dynamicTest("하나도 없는 경우", () -> {
//given
int strikeCount = 0;
int ballCount = 0;
String result = String.format(EMPTY_FORMAT);
init();
//when
Computer.hintMessage(strikeCount,ballCount);
//then
printOutput();
assertThat(getOutput().trim()).isEqualTo(result);
}),
DynamicTest.dynamicTest("3개의 숫자를 모두 맞힐 경우", () -> {
//given
int strikeCount = 3;
int ballCount = 0;
String result = String.format(STRIKE_FORMAT, strikeCount);
init();
//when
Computer.hintMessage(strikeCount,ballCount);
//then
printOutput();
assertThat(getOutput().trim()).isEqualTo(result);
})
);
}
private void init() {
standardOut = System.out; //표준 스트림 초기화
captor = new ByteArrayOutputStream();
System.setOut(new PrintStream(captor));
}
private void printOutput() {
System.setOut(standardOut); // 표준 스트림 할당
System.out.println(getOutput()); // 원하는 내용이 잘 나왔는지 문자열 디코딩 바이트를 가져와 출력
}
private String getOutput() {
// 기본 문자집합을 사용하여 버퍼의 내용을 문자열 디코딩 바이트로 변환
return captor.toString().trim();
}
init() 메서드에 @BeforeEach, printOutput() 메서드에 @AfterEach를 활용해도 좋겠죠?
저는 다른 테스트들도 있어서 전체 테스트가 돌아갈 때 불필요한 메서드 호출이 일어나는 거 같아서 사용하지 않았습니다.
- 다르게 생각하시는 분들은 의견주시면 감사하겠습니다!
참조
'개발' 카테고리의 다른 글
매직넘버, 리터럴 어디까지 상수 처리해야 돼? (4) | 2023.11.06 |
---|---|
1주 차 피드백을 2주 차 과제에 적용하기까지 (0) | 2023.11.01 |
원시 타입을 포장하자! (0) | 2023.10.31 |
Spring에서 Redis Geofencing기능 활용하기 (0) | 2023.10.06 |
spring Schedule + Event를 이용해서 redis Key값 넣어주기 (2) | 2023.10.06 |