기존 정산 legacy 시스템을 개선하는 과정의 일환으로 테스트코드도 같이 작성해보았다.
다른 개발자가 인수인계 받을 상황을 고려하여, 테스트 코드를 보았을때 프로젝트 구조를 파악할 수 있도록 해보았다.
또한 테스터블(Testable)한 로직 작성을 통해 가독성을 개선하였고, Clean Architecture에 기반하여 조금이나마 legacy 시스템의 유지편의성을 향상할 수 있도록 테스트 코드를 작성해보았다.
이에 대한 내용 기록은, Testable한 테스트 코드를 작성하기 위해 Test Double을 최대한 활용해보았던 이력을 중심으로 정리하여 남긴다.
일단 테스트 대상을 컨트롤러에서 실제 사용하는 부분인 "Provider"로 지정하였다.
최초 테스트 코드 작성 시에는 Provider의 동작 검증을 중점으로 작성하였는데, 사실 통합테스트의 관점보다는 테스트 가능한 코드가 아니기에 모든 기능을 하나의 통합 테스트로만 검증하는 "단방향적 Monolithic" 테스트 코드라는 생각이 너무 들었다.
@Test
void testCheckExcelForDuplicates_withGeneratedWorkbook() throws Exception {
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("Sheet1");
Row row = sheet.createRow(0);
row.createCell(0).setCellValue("설비코드");
row = sheet.createRow(1);
row.createCell(0).setCellValue("A001");
row = sheet.createRow(2);
row.createCell(0).setCellValue("A001"); // 중복 값
ByteArrayOutputStream bos = new ByteArrayOutputStream();
workbook.write(bos);
workbook.close();
try (InputStream is = new ByteArrayInputStream(bos.toByteArray())) {
int duplicates = CheckerProvider.checkExcelForDuplicates(is, "설비코드");
assertEquals(1, duplicates); // 기대 중복 수
}
}
이 테스트 코드를 좀 더 잘게 분리하여 무거운 테스트 코드를 쪼개고, 테스트 중복이 일어날 수 있는 여지를 최대한 제거하기 위해 단위 테스트 코드를 작성하였다.
그런데 단위 테스트 코드로 쪼개다보니까 예상치 못한 문제들이 너무 많았다.
일단 내부적으로 사용하는 OPC Package는 static 메소드만 제공하기에, 해당 객체만 Mock화하거나 Mocking 검증하는 과정은 불가능하였다.
또한 시스템 구조 자체가 InputStream을 받아서 사용하는데, InputStream이 재활용이 불가능하여 더이상 로직을 분리할 수 없고 그대로 유지해야만 하는 상태였다.
그래서 일단 통합테스트 코드는 그대로 유지하되, 내부적으로 핸들러 정도만 단위 테스트를 할 수 있도록 추가 조치하도록 하였다.
두개의 통합 테스트와 단위 테스트를 모두 유지하는 것은 End to End가 정상적으로 완료될 것을 기대하고, "굳이" 두 테스트 코드를 작성한다기 보다 "안전장치"를 마련한다는 의도로 단위 테스트 코드를 추가로 작성해주는 것으로 마무리 하였다.
@Test
void testDuplicateCheckerLogic() {
@Mock
OPCPackage opcPackage;
@Mock
XSSFReader xssfReader;
@Mock
SharedStringsTable sharedStringsTable;
@Mock
StylesTable stylesTable;
//when
SAXExcelDuplicateChecker checker = new SAXExcelDuplicateChecker("설비코드");
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("Sheet1");
Row row = sheet.createRow(0);
row.createCell(0).setCellValue("설비코드");
row = sheet.createRow(1);
row.createCell(0).setCellValue("A001");
row = sheet.createRow(2);
row.createCell(0).setCellValue("A001"); // 중복 값
//given
when(xssfReader.getSharedStringsTable()).thenReturn(sharedStringsTable);
when(xssfReader.getStylesTable()).thenReturn(stylesTable);
when(OPCPackage.open()).thenReturn(workbook);
XMLReader parser = XMLReaderFactory.createXMLReader();
ContentHandler sheetHandler = new XSSFSheetXMLHandler(
styles,
null,
sst,
handler,
formatter,
false
);
parser.setContentHandler(sheetHandler);
try(InputStream sheetStream = reader.getSheetsData().next()) {
parser.parse(new InputSource(sheetStream));
}
//then
assertEquals(2, checker.getResultList().size());
}
여기서 테스트가 "가능하도록" Testable에 신경쓴 부분은 Checker, Provider이다.
다만 대용량 Excel 데이터 처리를 위해 SAXExcel Parser를 사용한 부분이나 InputStream 재활용불가, Mock객체화 불가 등 프로젝트 전체적으로 변경이 어려운 부분이 많아, 현재 상태에서 최대한 테스트 가능한 코드를 작성할 수 있도록 구조를 분리하고 테스트에 적용할 수 있도록 개선해보았다.
Testable과 Test Double이 많이 헷갈렸는데, 이번에 테스트 코드를 작성하면서 개념을 정확히 이해할 수 있었다.
Test Double은 Mock, Stub 등 "테스트를 통한 검증"이 가능하도록 지원해주는 "도구"로 보면 된다.
@Mock UserRepository repo;
when(repo.findById(1L)).thenReturn(user); // ➜ Stub 역할
verify(repo).save(user); // ➜ Mock 역할
예를 들어, repo의 특정 상태값을 stub으로 정의하고 특정 동작을 mock으로 검증하고자 할 땐, 반드시 해당 객체를 Mock으로 선언해주는 @mock 작업이 먼저 선행되어야 한다.
이 부분을 자꾸 Mock 검증(동작 검증을 위한 then 단계 혹은 이를 위해 활용하는 객체) 개념과 일치하려고 하다보니까 지금까지 그렇게 헷갈렸던 것이다.
@Mock만 선언해놓고 아무런 Stub(when(...).thenReturn(...))도 하지 않으면,
그 mock 객체의 메소드들은 기본값(null, 0, false 등)만 반환하고 아무 동작도 하지 않는다.
따라서 기대한 동작이나 상태 결과가 없다면 테스트 코드로서 의미가 있다고 보기 어려우므로, 기대한 상태값이나 동작을 적절히 "정의해주는" 작업이 반드시 필요하다.
특정 객체의 상태값을 "정의", mock객체가 특정 동작 혹은 메소드를 호출했을때 반환하는 상태값을 고정적으로 "보장"해주기 위한 Test Double이다.
when(repo.findById(1L)).thenReturn(user); // ➜ Stub 역할
verify(repo).save(user); // ➜ Mock 역할
동작을 "수행하는지" "검증"해주는 Test Double이다. 동작의 반환값에는 관심없고, 동작의 호출여부 혹은 동작여부 등만 검증한다.
일단 Test Double이 테스트 코드에 이용하는 도구라 하여, 무조건적으로 "검증"을 위한 것으로 이해한다면 곤란할 것 같다.
테스트코드를 왜 진행하고, 이를 위해 어떠한 지원이 있고 어떠한 도구를 활용할 수 있는지, Test Double과 연관지으면서 이해한다면 "올바른 방향"의 테스트 코드를 작성할 수 있을 것이다.
여기에 Testable과 Test Double의 차이점을 잘 이해하면서 테스트 코드를 작성한다면, TDD와 테스트 코드 작성 및 프로젝트 Clean Architecture 등..여러 연관 개념들을 명확하게 분리하고 정리할 수 있을 것이다.
그렇게 어려웠던 테스트코드, TDD에 대한 개념이 서서히 잡혀가는 것을 느낀다.