목차
InputValidator
InputValidator.java
이곳에서는 인풋으로 받은 값들을 검증합니다. 따라서 두개의 메서드 전부 예외를 알맞게 처리하는지 테스트 해보기로 했습니다.
public class InputValidator {
private static final int MAXIMUM_LENGTH_OF_NAME = 5;
public boolean lengthOfCarNameValidate (String carName) {
if(carName.length() > MAXIMUM_LENGTH_OF_NAME) {
throw new IllegalArgumentException("The name must be no more than five characters long.");
}
return true;
}
public boolean gameCountValidate (String gameCount) {
if (!Pattern.matches("^[0-9]+$", gameCount)) {
throw new IllegalArgumentException("Only numbers can be entered");
}
return true;
}
}
Java
복사
InputValidatorTest.java
class InputValidatorTest {
private InputValidator inputValidator; <- 일단 테스트할 클래스를 끌고와서
@BeforeEach
void setUp() {
inputValidator = new InputValidator(); <- 각각의 테스트를 수행 하기전에 새로 만들어줍니다.
}
@Test
void 이름_길이에_대한_예외_처리() {
String validName = "sieun";
String invalidName = "sieunnnn";
assertDoesNotThrow(() -> inputValidator.lengthOfCarNameValidate(validName));
assertThrows(IllegalArgumentException.class, () -> inputValidator.lengthOfCarNameValidate(invalidName));
}
@Test
void 게임_횟수에_대한_예외_처리() {
String validGameCount = "1";
String invalidGameCount = "a";
assertDoesNotThrow(() -> inputValidator.gameCountValidate(validGameCount));
assertThrows(IllegalArgumentException.class, () -> inputValidator.gameCountValidate(invalidGameCount));
}
}
Java
복사
ResultGenerator
ResultGenerator.java
이곳에서는 게임을 진행하며 회당 결과를 생성합니다. 이 중, 랜덤 넘버에 따라 전진할지 정지할지 결정하는 메서드를 테스트 해보기로 하였습니다.
public class ResultGenerator {
private static final int MOVING_FORWARD = 4;
public void generateResult(List<String[]> cars) {
for (int i = 0; i < cars.size(); i ++) {
int randomNumber = Randoms.pickNumberInRange(0, 9);
if (determineMoveOrStop(randomNumber)) {
moveCar(cars, i);
}
System.out.println(cars.get(i)[0] + " : " + cars.get(i)[1]);
}
System.out.println();
}
private boolean determineMoveOrStop(int randomNumber) {
if (randomNumber >= MOVING_FORWARD) {
return true;
}
return false;
}
private void moveCar(List<String[]> cars, int index) {
cars.get(index)[1] += "-";
}
}
Java
복사
테스트 하려는 메서드가 private 인 경우에는?
determineMoveOrStop 메서드만 테스트 하고싶은데, 해당 메서드는 캡슐화를 위해 private 접근 제한자를 사용하였습니다. 코드를 변경하지 않고 테스트를 하려면 java 의 reflection 을 사용하여야 합니다.
reflection 이란?
Reflection 은 자바에서 제공하는 API 로써 런타임에서 객체의 정보를 조회하거나 변경하는 기능을 제공합니다. 이를 통해 개발자는 클래스, 필드, 메서드, 매개변수 등에 대한 메타데이터를 가져올 수 있고, 동적으로 객체를 생성하거나 메서드를 호출할 수 있습니다.
이를 사용하는게 좋은걸까?
일반적으로 private 메서드를 테스트하고 싶은 경우 private 메서드가 호출되는 public 이나 protected 메서드를 테스트하여 간접적으로 해당 메서드가 정상 작동 되는지 확인합니다. reflection 을 사용하는 경우 코드의 복잡성이 증가되고, 리팩토링 등의 변경에서 테스트가 깨질 확률이 높아진다고 합니다.
근데 왜 사용했나요?
제가 하려는 테스트의 경우 단순 하므로 사용했습니다. 오히려 간접적으로 정상 작동 되는지 확인하려면 더 복잡한 테스트 코드를 작성해야했습니다. 이는 제 코드가 잘못됨을 시사하는 걸 수도 있습니다. 따라서 해당 주차 제출이 완료되면 다른 사람들에게 코드 리뷰를 받으며 시야를 넓혀가야겠습니다. (배울게 정말 많군요)
ResultGeneratorTest.java
public class ResultGeneratorTest {
private ResultGenerator resultGenerator;
@BeforeEach
void setUp() {
resultGenerator = new ResultGenerator();
}
@Test
void 랜덤_넘버에_따른_전진_정지() throws Exception {
// determineMoveOrStop 메서드를 가져옴
Method method = ResultGenerator.class.getDeclaredMethod("determineMoveOrStop", int.class);
// private 접근 제한자를 무시하도록 설정
method.setAccessible(true);
assertTrue((Boolean) method.invoke(resultGenerator, 4)); // 4 이상 이면 true
assertTrue((Boolean) method.invoke(resultGenerator, 5)); // 5 도 true
assertFalse((Boolean) method.invoke(resultGenerator, 3)); // 3 이하면 false
}
}
Java
복사
WinnerFinder
WinnerFinder.java
이곳에서는 게임의 우승자를 선별합니다. 우승자가 단수 일 수도 있지만 복수 일 수도 있기 때문에 두 가지의 경우를 테스트 해보기로 하였습니다.
public class WinnerFinder {
public void printWinner(List<String[]> cars) {
int max = findMaxLength(cars);
findWinners(cars, max);
}
private int findMaxLength(List<String[]> cars) {
cars.sort(Comparator.comparingInt(car -> car[1].length()));
return cars.get(cars.size() - 1)[1].length();
}
private void findWinners(List<String[]> cars, int max) {
List<String> winners = new ArrayList<>();
for (String[] car : cars) {
if (car[1].length() == max) {
winners.add(car[0]);
}
}
String result = String.join(", ", winners);
System.out.println("최종 우승자 : " + result);
}
}
Java
복사
테스트 하려는 메서드에 System.out.println 이 있는 경우에는?
System.out 출력 스트림 가로채기
findWinners 의 결과과 예상 결과가 같은지 테스트 하기 위해서는 System.out 의 출력 스트림을 가로채어 ByteArrayOutputStream 에 저장합니다. 테스트를 마친후에는 스트림을 다시 원복해주어야 합니다. 원복하지 않으면 테스트의 실행 이후로 모든 System.out.println() 호출의 결과가 ByteArrayOutputStream 에 저장되며, 콘솔에는 아무 것도 출력되지 않게 됩니다.
WinnerFinderTest.java
public class WinnerFinderTest {
private WinnerFinder winnerFinder;
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private final PrintStream originalOut = System.out;
@BeforeEach
public void setup() {
winnerFinder = new WinnerFinder();
System.setOut(new PrintStream(outContent));
}
@Test
public void 최종_단일_우승자_선별() {
List<String[]> cars = new ArrayList<>();
cars.add(new String[]{"carA", "-"});
cars.add(new String[]{"carB", "--"});
cars.add(new String[]{"carC", "---"});
winnerFinder.printWinner(cars);
String expectedOutput = "최종 우승자 : carC\n";
assertEquals(expectedOutput, outContent.toString());
}
@Test
public void 최종_복수_우승자_선별() {
List<String[]> cars = new ArrayList<>();
cars.add(new String[]{"carA", "-"});
cars.add(new String[]{"carB", "--"});
cars.add(new String[]{"carC", "---"});
cars.add(new String[]{"carD", "---"});
winnerFinder.printWinner(cars);
String expectedOutput = "최종 우승자 : carC, carD\n";
assertEquals(expectedOutput, outContent.toString());
}
@AfterEach
public void restoreStreams() { // 테스트 수행 후 원복
System.setOut(originalOut);
}
}
Java
복사
회고
이게 맞는지는 모르겠습니다만,, 일단 열심히 짜보긴 했습니다. 테스트 코드 작성하는 것 중요하지만 학습 곡선이 어느정도 존재하기에 도전하기 쉽지 않죠. 요새 TDD 다 클린코드다 말이 너무 많이 나오고, 마치 따르지 않으면 삽시간에 이상한 사람이 되버리는 경향이 존재하기 때문에 개발 초보들도 아무 생각없이 따라하는 부작용이 생기게 되는것 같습니다.
우테코의 경우 매 주차마다 개발자가 가져야 하는 소양을 조금씩 쉽게 접할 수 있도록 해주는 점이 좋은 것 같습니다. 미루고 미뤄왔던 테스트 코드 공부를 결국엔 하게… 되었으니까요..?
앞으로 남은 2 회차 더 열심히해서 좋은 결과를 이루도록 노력해봐야겠습니다