최근 TDD에 관심이 생겨서 JUnit을 공부하고 있던 중 알고리즘 문제 풀이에 이를 적용할 수 있겠다는 생각이 들었습니다.
그래서 처음에는 String[]에 입력값을 넣고 출력값을 무조건 String으로 저장하여 assertEquals로 확인하는 방법을 사용했습니다.
그런데 입력이나 출력값이 많아지면 많아질수록 테스트 코드에 값을 추가하는 과정이 힘들어지기 때문에 문제점을 해결하고자 했습니다.
그래서 일반적으로 문제풀이에서 입력과 출력에 대한 내용이 담겨 있는 .in, .out 파일을 통해 자신의 문제 풀이가 정확한지 테스트하는 방법을 구상하게 되었습니다.
본론
문제풀이 Class 구성
저는 문제를 풀 때 항상 다음과 같은 구조로 클래스를 정의합니다.
public class Main{
private static BufferedReader bf;
static void input(){
//입력받은 데이터들로 문제 풀 준비를 하는 메서드
...
}
static void process(){
//문제 풀이 과정이 담긴 메서드
...
System.out.println(result);
}
public static void main(String[] args){
bf = new BufferedReader(new InputStreamReader(System.in));
input();
process();
}
}
위 코드에서 System.in과 System.out은 기본 입출력을 의미합니다. 자바에서는 기본 입출력을 다르게 설정할 수 있습니다.
이와 관련한 자세한 내용은 자바의 Stream과 Reader를 찾아보면 좋을 것 같습니다.
테스트 Class 구성
System.in과 System.out을 테스트케이스의 in, out파일에 대한 스트림으로 설정하여 마치 테스트케이스를 직접 입력하는 것과 같은 효과를 보일 수 있습니다.
아래 코드처럼 System.in과 System.out은 변수에 저장할수도 있고 새로 설정할수도 있습니다.
테스트케이스가 여러 개인 경우 자동화를 위해서 JUnit4의 Parameterized를 사용하여 파일을 불러옵니다.
in, out 파일
문제풀이 사이트에서는 각각의 테스트케이스를 검사하기 위해 위와 같이 in, out 파일을 통해 일치하는지 확인합니다.
아래는 주어지는 테스트케이스를 in, out 파일로 저장하고 이를 모두 불러와 자동으로 테스트하는 기본 구조입니다.
@RunWith(Parameterized.class)
public class MainTest{
private static final String TESTCASE_PATH = "src/testdirector/testcase";
// in, out 파일을 읽어들일 Stream과 Reader
private FileInputStream inFileStream;
private BufferedReader outFileReader;
// System.out으로 출력된 값들을 모아둘 Stream
private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 기본 Stream 저장
private InputStream originalIn = System.in;
private PrintStream originalOut = System.out;
public MainTest(String testName) throws IOException{
// testcase의 in, out 파일의 Stream을 연결
this.inFileStream = new FileInputStream(String.format("%s/%s.in", TESTCASE_PATH, testName));
this.outFileReader = new BufferedReader(new FileReader(String.format("%s/%s.out", TESTCASE_PATH, testName)));
}
@Before
public void setUp() throws IOException{
// 테스트 시작 전 기본 입출력 설정
System.setIn(inFileStream);
System.setOut(new PrintStream(outputStream));
}
@After
public void rollBack(){
System.setIn(originalIn);
System.setOut(originalOut);
}
@Parameters(name="{index} - {0}")
public static Object[] data(){
File parentDir = new File(TESTCASE_PATH);
// 확장자를 제거하고 파일이름만 배열로 전달
return Arrays.stream(parentDir.list())
.map(testName -> testName.replaceFirst("[.][^.]+$", ""))
.collect(Collectors.toSet())
.toArray(String[]::new);
}
@Test
public void test() throws IOException{
Main.main(null);
assertArrayEquals(outFileReader.lines().toArray(), outputStream.toString().split("\n"));
}
}
테스트케이스가 추가될 때마다 같은 이름의 in, out 파일만 디렉토리에 만들어주면 이전것들과 함께 테스트가 이루어지게 됩니다.
결론
이전에는 문제풀이 할 때 커맨드라인에 한줄한줄 입력하면서 진행했었는데, 얼마나 비효율적인지 깨닫게 되는 것 같습니다. 만약 TDD를 배워야 하는 입장이라면 문제풀이 과정에 적용하여 보는 것도 좋은 것 같습니다. 테스트 시간 단축 및 TDD 기본 학습에 도움이 많이 됩니다.