2024. 10. 24. 17:26ㆍ우아한테크코스
1주 차 피드백을 바탕으로 TDD를 적용하려고 하니, 생각지도 못한 큰 난관에 부딪혔다.
바로 테스트 툴에 대한 이해가 부족하다는 것...
그렇게나 공부하고 많은 예시를 봤음에도 직접 경험하지 않는다면 무용지물이다.
추상적으로만 생각했을 때, 단순히 실패하는 테스트 케이스를 작성하고, 그 테스트가 통과하도록 코드를 작성한 후, 리팩토링하면 된다고 생각했다. 하지만 실제로는 그 과정이 훨씬 더 복잡했다. 테스트 케이스를 어떻게 작성해야 할지도 감이 잘 오지 않았고, 무엇보다 테스트가 서로 의존적이지 않게 구성하는 방법에 대해서도 막막했다. 특히 이번 과제는 표준 입력을 다루는 특성상, 이를 테스트하는 방법이 쉽지 않았다.
이전에 사용해 본 Mockito 같은 라이브러리를 사용한다면 해결할 수 있을까 싶었지만,
어림도 없었다. 물론 Mockito가 해답이라는 것은 아니다.
1주 차 테스트 코드
그럼 1주 차의 나는 어떻게 테스트 코드를 작성했을까?
- 테스트 코드의 정석(?)으로 불리는 패턴, Given-When-Then을 사용하지 않음.
- 제공된 기본 ApplicationTest 클래스 형태를 그대로 참고해서 작성함.
- 기능 구현 이후, 엣지 케이스를 위한 테스트를 작성함.
1주 차에는 로직을 모두 구현한 후, 엣지 케이스를 위해 테스트 코드를 작성했다. 그래서 이는 TDD 방식의 단위 테스트가 아닌, 통합 테스트로 진행된 것을 확인할 수 있다.
2주 차 테스트 코드
2주 차에 제공된 테스트 코드를 봤을 때도 통합 테스트가 제공된 것을 확인할 수 있다.
입력 값을 검증하는 것부터 TDD 방식으로 구현하고 싶었는데, 어떻게 해야 할지 막막했다.
NsTest
결론부터 말하자면, 내가 원하는 테스트들은 모두 가능했다.
위에서 설명한 테스트 클래스들의 공통점이 있다. NsTest 추상 클래스를 상속받았다는 점이다.
NsTest... 완전히 뜯어보자!
@BeforeEach
protected final void init() {
this.standardOut = System.out; // 기존 표준 출력을 저장 (나중에 복원하기 위해)
this.captor = new ByteArrayOutputStream(); // 출력 내용을 메모리로 저장할 스트림
System.setOut(new PrintStream(this.captor)); // 표준 출력을 가로채서 captor에 저장
}
@BeforeEach: 각 테스트 전에 출력 스트림을 가로채고, 모든 출력이 메모리로 저장되도록 설정
@AfterEach
protected final void printOutput() {
System.setOut(this.standardOut); // 표준 출력을 원래 상태로 복구
System.out.println(this.output()); // 캡처된 출력 결과를 콘솔에 출력
}
@AfterEach: 각 테스트 후 출력 스트림을 복구하고, 캡처한 출력 내용을 출력
이렇게 하면 테스트 중 출력된 내용을 메모리에 저장할 수 있으며, 나중에 이를 통해 출력 결과를 검증할 수 있는 것을 확인했다.
protected final void run(String... args) {
try {
this.command(args); // 입력값을 설정
this.runMain(); // 프로그램의 main() 메서드를 실행
} finally {
Console.close(); // 리소스 해제
}
}
기본으로 제공된 테스트 코드마다 항상 보이던 run 메서드다!
command, runMain를 확인해봐야 할 것 같다.
private void command(String... args) {
byte[] buf = String.join("\n", args).getBytes(); // 입력값을 \n로 연결하여 바이트 배열로 변환
System.setIn(new ByteArrayInputStream(buf)); // 가상의 입력값을 System.in에 설정
}
입력값을 설정하는 역할을 한다.
정확히는 run에서 전달된 args를 System.in에 설정하여 프로그램이 이를 입력으로 사용할 수 있게 만든다.
protected abstract void runMain();
추상 메서드 runMain은 통합 테스트에서는 프로그램 진입점인 Application.main을 실행하도록 구현할 수 있다.
단위 테스트의 경우, 바디를 비워도 괜찮을 것 같다.
protected final void runException(String... args) {
try {
this.run(args); // run() 메서드를 실행
} catch (NoSuchElementException var3) {
// 예외가 발생해도 무시
}
}
입력이 비정상적으로 종료되거나 입력 값이 없는 경우를 처리하는 것 같다.
결국, run 메서드는
- 가상의 입력값을 설정하고, -> command
- 이를 통해 프로그램을 실행하여, -> runMain
테스트 중 실제 사용자 입력을 시뮬레이션하는 역할을 합니다.
사용 예시
제공된 NsTest를 상속받아 테스트 코드를 작성할 때, 1주 차 리뷰, 피드백을 고려했습니다. (Given-When-Then 패턴을 사용)
우테코에서 제공해 준 NsTest 덕분에 run 호출만으로 간단하게 가상의 입력 값을 테스트할 수 있었다!
출력 검증에도 유용하게 사용할 수 있을 것 같다.
NsTest 상속을 제거하고, System.setIn(), setOut()을 활용하여 입력 스트림을 직접 설정하는 방법을 참고할 수도 있다.
결론
제공된 도구들을 활용하여 테스트에서 입출력을 검증하는 방법을 배울 수 있었다.
그 외에도, test 패키지 내에 다양한 검증 클래스와 메서드들이 있다.
테스트 작성이나 리팩토링 시 이를 적극 활용하는 것도 고려해 봐야겠다.
'우아한테크코스' 카테고리의 다른 글
[우테코 7기 프리코스 2주 차] 회고 (0) | 2024.11.01 |
---|---|
[우테코 7기 프리코스 2주 차] TDD (0) | 2024.10.31 |
[우테코 7기 프리코스 2주 차] 옵저버 패턴 (0) | 2024.10.30 |
[우테코 7기 프리코스 1주 차] 피드백 (0) | 2024.10.22 |
[우테코 7기 프리코스 1주 차] 객체 지향 (3) | 2024.10.21 |