세 번째 TDD 연습에서는 테스트 코드 자체를 좀 정리할 것이다. 연습1과 2를 통해서 TranscodingServiceImpl 클래스의 정상 동작인 경우와 비정상 동작인 경우에 대해서 테스트를 진행하였다. 그런데 테스트 코드를 만들면서 중복된 코드들이 많이 출현하였다.
먼저 다음의 두 메서드를 비교해 보자.
@Test
public void transcodeSuccessfully() {
when(jobRepository.findById(jobId)).thenReturn(mockJob);
File mockMultimediaFile = mock(File.class);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
List<File> mockMultimediaFiles = new ArrayList<File>();
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
List<File> mockThumbnails = new ArrayList<File>();
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
Job job = jobRepository.findById(jobId);
assertTrue(job.isWaiting());
transcodingService.transcode(jobId);
job = jobRepository.findById(jobId);
assertTrue(job.isFinished());
assertTrue(job.isSuccess());
assertEquals(Job.State.COMPLETED, job.getLastState());
assertNull(job.getOccurredException());
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
verify(createdFileSender, only()).store(mockMultimediaFiles,
mockThumbnails, jobId);
verify(jobResultNotifier, only()).notifyToRequester(jobId);
}
@Test
public void transcodeFailBecauseExceptionOccuredAtJobResultNotifier() {
when(jobRepository.findById(jobId)).thenReturn(mockJob);
File mockMultimediaFile = mock(File.class);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
List<File> mockMultimediaFiles = new ArrayList<File>();
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
List<File> mockThumbnails = new ArrayList<File>();
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
RuntimeException mockException = new RuntimeException();
doThrow(mockException).when(jobResultNotifier).notifyToRequester(jobId);
Job job = jobRepository.findById(jobId);
assertTrue(job.isWaiting());
try {
transcodingService.transcode(jobId);
fail("발생해야 함");
} catch (Exception ex) {
assertSame(mockException, ex);
}
job = jobRepository.findById(jobId);
assertTrue(job.isFinished());
assertFalse(job.isSuccess());
assertEquals(Job.State.NOTIFYING, job.getLastState());
assertNotNull(job.getOccurredException());
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
verify(createdFileSender, only()).store(mockMultimediaFiles,
mockThumbnails, jobId);
verify(jobResultNotifier, only()).notifyToRequester(jobId);
}
}
굵게 표시한 부분은 테스트를 위한 Mock 객체를 초기화하는 부분으로 다른 테스트 메서드도 일부 다른 부분이 있지만, 거의 유사하다. 먼저 다음과 같이 코드를 수정한다.
- mockMultimediaFile 변수, mockMultimediaFiles 변수, mockThumbnails 변수를 필드로 변경한다.
- 각 테스트 메서드에서 세 변수 선언 부분을 삭제한다.
변경된 코드는 다음과 같다.
@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
...
// 각 테스트 메서드에서 공통으로 사용되는 로컬 변수를 필드로 전환
private File mockMultimediaFile = mock(File.class);
private List<File> mockMultimediaFiles = new ArrayList<File>();
private List<File> mockThumbnails = new ArrayList<File>();
...
@Test
public void transcodeSuccessfully() {
// 필드로 바뀐 로컬 변수 선언 삭제됨
when(jobRepository.findById(jobId)).thenReturn(mockJob);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
...
// verify에서 필드 사용
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
...
}
// 나머지 테스트 코드도 동일하게 변경
...
@Test
public void transcodeFailBecauseExceptionOccuredAtJobResultNotifier() {
when(jobRepository.findById(jobId)).thenReturn(mockJob);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
...
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
...
}
}
뭔가 바꿨으니 테스트를 실행해 본다. 녹색! 문제가 발생하지 않았다. 이제 다음 중복으로 넘어가자. 또 다른 중복되는 부분은 Mock 객체의 행위를 지정하는 부분이다. (when().thenReturn() 류의 코드.) 이런 코드들 역시 각 테스트 메서드에서 중복해서 출현하므로 setUp() 메서드로 이동시키자.
@Before
public void setup() {
transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
transcoder, thumbnailExtractor, createdFileSender,
jobResultNotifier, jobStateChanger, transcodingExceptionHandler);
// 각 테스트 메서드의 중복되는 초기화 코드를 setup으로 이동
when(jobRepository.findById(jobId)).thenReturn(mockJob);
when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
mockMultimediaFiles);
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenReturn(
mockThumbnails);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Job.State newState = (State) invocation.getArguments()[1];
mockJob.changeState(newState);
return null;
}
}).when(jobStateChanger).chageJobState(anyLong(), any(Job.State.class));
...
}
@Test
public void transcodeSuccessfully() {
// 초기화 코드 제거됨
Job job = jobRepository.findById(jobId);
assertTrue(job.isWaiting());
transcodingService.transcode(jobId);
...
}
@Test
public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
// 초기화 코드 제거됨
...
}
@Test
public void transcodeFailBecauseExceptionOccuredAtTranscoder() {
// 초기화 코드 제거됨
RuntimeException mockException = new RuntimeException();
when(transcoder.transcode(mockMultimediaFile, jobId)).thenThrow(
mockException);
...
}
@Test
public void transcodeFailBecauseExceptionOccuredAtThumbnailExtractor() {
// 초기화 코드 제거됨
RuntimeException mockException = new RuntimeException();
when(thumbnailExtractor.extract(mockMultimediaFile, jobId)).thenThrow(
mockException);
...
try {
transcodingService.transcode(jobId);
fail("발생해야 함");
} catch (Exception ex) {
assertSame(mockException, ex);
}
}
@Test
public void transcodeFailBecauseExceptionOccuredAtCreatedFileSender() {
// 초기화 코드 제거됨
RuntimeException mockException = new RuntimeException();
doThrow(mockException).when(createdFileSender).store(
mockMultimediaFiles, mockThumbnails, jobId);
...
}
@Test
public void transcodeFailBecauseExceptionOccuredAtJobResultNotifier() {
// 초기화 코드 제거됨
RuntimeException mockException = new RuntimeException();
doThrow(mockException).when(jobResultNotifier).notifyToRequester(jobId);
...
}
뭔가를 바꿨으니 또 테스트다. 녹색! 통과다.
이제 실패의 경우를 테스트하는 5개의 테스트 메서드에서 중복을 제거하자. 실패 시나리오를 테스트하는 두 메서드를 보자.
@Test
public void transcodeFailBecauseExceptionOccuredAtCreatedFileSender() {
RuntimeException mockException = new RuntimeException();
doThrow(mockException).when(createdFileSender).store(
mockMultimediaFiles, mockThumbnails, jobId);
try {
transcodingService.transcode(jobId);
fail("발생해야 함");
} catch (Exception ex) {
assertSame(mockException, ex);
}
Job job = jobRepository.findById(jobId);
assertTrue(job.isFinished());
assertFalse(job.isSuccess());
assertEquals(Job.State.STORING, job.getLastState());
assertNotNull(job.getOccurredException());
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
verify(createdFileSender, only()).store(mockMultimediaFiles,
mockThumbnails, jobId);
verify(jobResultNotifier, never()).notifyToRequester(jobId);
}
@Test
public void transcodeFailBecauseExceptionOccuredAtJobResultNotifier() {
RuntimeException mockException = new RuntimeException();
doThrow(mockException).when(jobResultNotifier).notifyToRequester(jobId);
Job job = jobRepository.findById(jobId);
assertTrue(job.isWaiting());
try {
transcodingService.transcode(jobId);
fail("발생해야 함");
} catch (Exception ex) {
assertSame(mockException, ex);
}
job = jobRepository.findById(jobId);
assertTrue(job.isFinished());
assertFalse(job.isSuccess());
assertEquals(Job.State.NOTIFYING, job.getLastState());
assertNotNull(job.getOccurredException());
verify(mediaSourceCopier, only()).copy(jobId);
verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
verify(createdFileSender, only()).store(mockMultimediaFiles,
mockThumbnails, jobId);
verify(jobResultNotifier, only()).notifyToRequester(jobId);
}
굵게 표시한 부분이 중복된 코드이다. 이 중복을 다음과 같이 제거해 보자.
- mockException 로컬 변수를 필드로 전환
- try - catch 블록과 job의 상태를 검사하는 코드를 메서드로 분리