Search
👩🏻‍🔬

JUnit5 와 AssertJ 로 테스트 코드 작성하기

태그
JUnit5
AssertJ
목차

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 회차 더 열심히해서 좋은 결과를 이루도록 노력해봐야겠습니다