알고리즘 테스트용 JUnit 테스트 만들기

임현규·2023년 4월 11일
0

자바로 알고리즘 문제들 테스트하기

자바로 요즘 알고리즘 풀이 사이트 문제 형식에 제공되는 파라미터를 기입하는게 생각보다 오래 걸리고 귀찮다. 대표적인 예로 프로그래머스가 있는데 프로그래머스의 입력은 다음과 같이 이루어 진다.

이걸 java junit으로 테스트하려면 [ 와 ] 를 {}로 바꾸어서 기입해야한다.
문제가 2차원 배열이면 이것은 더 복잡해진다. 실제 자바에서 테스트하려면 다음과 같이 써야 한다

@Test
void test() {
	String[] board = new String[] {"O.X", ".O.", "..X"};
    int expect = 1;
    Solution solution = new Solution();
    int result = solution.solution(board);
    assertThat(result).isEqualTo(expect);
}

이런 코드를 여러개 작성해야 한다. 그러나 솔직히 시간이 없는데 대괄호를 중괄호로 바꿔서 테스트 코드에 넣어보고 일일히 코드 짜고 하는 것은 실제 시험에서 부담감이 심하다. 이를 해결하기 위해 최대한 적은 코드로 많은 코드를 통합 테스트 할 수 있어야 한다.

@ParameterizedTest와 @CsvSource 활용하기

여러 매개변수를 쓰고 여러 파라미터를 정의해서 쓰기 좋은 어노테이션은 @ParameterizedTest와 @CsvSource이다. 그러나 이것 둘의 문제점은 배열의 경우 효과적으로 정의하기 어렵고 코테처럼 대활호 안에 숫자 이런식으로 활용하기가 어렵다. 그래서 추가적으로 타입 변환을 해줄 argument type converter가 필요하다.

1. SimpleArgumentConverter 활용하기

SimpleArgumentConvert를 상속해서 Convert로직을 짜준다. 코드는 다음과 같다.

public class IntArray extends SimpleArgumentConverter {

    private static final Pattern pattern = Pattern.compile("\\[(\\d+(,\\s*\\d+)*)\\]");
    private static final String DELIMITER = ",";

    @Override
    protected Object convert(Object source, Class<?> targetType)
        throws ArgumentConversionException {
        if (source instanceof String && int[].class.isAssignableFrom(targetType)) {
            Matcher matcher = pattern.matcher((String) source);
            if (matcher.find()) {
                String match = matcher.group(1);
                String[] numbers = match.replaceAll("\\s", "").split(DELIMITER);
                return Arrays.stream(numbers)
                    .mapToInt(Integer::parseInt)
                    .toArray();
            }
        }
        if (source instanceof String && List.class.isAssignableFrom(targetType)) {
            Matcher matcher = pattern.matcher((String) source);
            if (matcher.find()) {
                String match = matcher.group(1);
                String[] numbers = match.replaceAll("\\s", "").split(DELIMITER);
                return Arrays.stream(numbers)
                    .map(Integer::parseInt)
                    .collect(Collectors.toList());
            }
        }

        throw new IllegalArgumentException("Conversion from " + source.getClass() + " to "
            + targetType + " not supported.");
    }
}

코드를 보면 [1,2,3,4,5] 와 같은 문자열 패턴을 파싱할 정규식을 정의하고 @ConvertWith에서 정의한 타입을 검사후 split을 활용해 문자 또는 숫자를 추출한 후 타입변환을 해주는 로직이다. 이제 모든 준비는 끝났다.

2. @CsvSource와 @ConvertWith 활용하기

우리는 converter가 ","를 delimiter로 정했기 때문에 CsvSource에서는 " "로 정하자. (delimiter가 겹처서 발생하는 에러를 막기 위함)

그러면 다음과 같은 코드를 짤 수 있다.

	@ParameterizedTest
    @Timeout(value = 1000, unit = TimeUnit.MILLISECONDS)
    @CsvSource(value = {
        "[1,2,3] [4,5,6]",
        "[1,2,5] [1,5,6]",
    }, delimiter = ' ')
    void test(@ConvertWith(IntArray.class) int[] array1,
        @ConvertWith(IntArray.class) List<Integer> array2
    ) {
        System.out.println(Arrays.toString(array1));
        System.out.println(array2);
    }

String 타입이면 해당 arrayConverter를 하나 만들어주면 된다.
어떤가? 한눈에 알아보기 편하고 코드 중복없이 파라미터를 활용한 통합테스트를 굉장히 빠르게 진행할 수 있다. 현업에서는 테스트별 경계값 또는 상황에 대한 특징이 있기 때문에 테스트 코드를 분리하는 것이 원칙이겠지만 시험에서는 이 방법이 빠르게 파악하는데 더 효과적일 것이다.

@CsvFileSource 활용

실제 csv파일을 연동하는 방법도 있다.

intellij에서 resources에 test.csv와 같은 파일을 만든다.


그 이후 csv 테이블을 이용해 마킹한다

delimiter를 바꾸고 싶다면 우클릭을 누르고 Separator를 누르면 바꿀 수 있다. 우리는 ','로 converter에서 구분하므로 해당 문자만 피하면 어느것을 써도 상관없다.

	@ParameterizedTest
    @CsvFileSource(resources = "/test.csv", numLinesToSkip = 1, delimiter = '\t')
    void test2(
        @ConvertWith(IntArray.class) int[] array1,
        @ConvertWith(IntArray.class) int[] array2
    ) {
        System.out.println(Arrays.toString(array1));
        System.out.println(Arrays.toString(array2));
    }

이제 @CsvSource와 같이 코드를 짜주면 된다. 본인이 편한 방식을 사용하면 된다!

결론

아직은 더 써봐야 알겠지만 자바로 알고리즘 테스트하는 것은 더 편해진 것 같기도 하다. 급조한 코드라 Converter가 조금 아쉬울 수도 있는데 생산성이 좋아지면 코드를 조금 더 보완해봐야겠다.

profile
엘 프사이 콩그루

0개의 댓글