저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'단위테스트'에 해당되는 글 1건

  1. 2012.10.23 TDD 연습 1. TranscodingService의 관련 객체 인터페이스 도출하기 (2)

* TDD 과정을 공유하기 위해 연습하는 과정을 글로 남긴다.


시작은 간단한 설계로


만들어 볼 소프트웨어는 동영상 파일 변환 시스템이다. 주요 기능을 다음과 같이 잡아 보았다.

  • 임의 위치의 미디어 파일을 읽어와 지정한 포맷으로 변환한 뒤 결과를 원하는 위치에 저장한다.
  • 원하는 방식으로 썸네일 이미지를 생성해서 원하는 위치에 저장한다.
  • 진행 상태를 중간 중간 확인할 수 있다.
  • 변환 결과를 통지 받는다.
보다 자세한 기능 명세가 필요하겠지만, 개요는 위와 같다. 변환 요청은 REST로 받을 수도 있고, 일반 웹 폼으로 받을 수도 있고, 소켓으로 받을 수도 있겠지만, UI는 핵심이 아니므로 쉽게 UI를 붙일 수 있는 구조로만 가면 될 뿐 최초 개발에 UI를 먼저 개발하진 않는다.

아주 간단하게 변환 처리와 관련된 부분의 설계를 먼저 해 보았다. 


최초 설계


변환 처리를 수행할 TranscodingService가 필요할 것 같았고, 그 외에 작업의 의미를 담는 Job 그리고 그 Job을 보관할 JobRepository가 필요하다고 판단했다. 


테스트 코드로 시작하기


자, 이제 할 작업은 TranscodingService를 위한 테스트를 만드는 것이었다.


가장 먼저 만든 코드는 다음과 같다.


public class TranscodingServiceTest {


    @Test

    public void transcodeSuccessfully() {

        // 미디어 원본으로부터 파일을 로컬에 복사한다.

        // 로컬에 복사된 파일을 변환처리한다.

        // 로컬에 복사된 파일로부터 이미지를 추출한다.

        // 변환된 결과 파일과 썸네일 이미지를 목적지에 저장

        // 결과를 통지

    }

}


transcodeSuccessfully() 메서드는 TranscodingService가 성공적으로 동작하는 상황을 테스트 하기 위해 만들었으며, 성공적으로 작업이 진행될 때 수행해야 할 작업을 주석으로 입력해 놓았다. 이 주석을 구현으로 바꿔치기 해 나가면서 TranscodingService를 점진적으로 완성해 나갈 것이다.


가장 먼저 해야 할 작업은 미디어 원본으로부터 파일을 로컬에 복사해 오는 부분을 구현하는 것이다. 자, 시작은 아래와 같다.


public class TranscodingServiceTest {


    @Test

    public void transcodeSuccessfully() {

        // 미디어 원본으로부터 파일을 로컬에 복사한다.

        Long jobId = new Long(1);

        File multimediaFile = copyMultimediaSourceToLocal(jobId);


        // 로컬에 복사된 파일을 변환처리한다.

        // 로컬에 복사된 파일로부터 이미지를 추출한다.

        // 변환된 결과 파일과 썸네일 이미지를 목적지에 저장

        // 결과를 통지

    }


    private File copyMultimediaSourceToLocal(Long jobId) {

        return mediaSourceCopier.copy(jobId); // 컴파일 에러!

    }

}


미디어 원본 파일을 로컬에 복사해주는 부분을 copyMultimediaSourceToLocal() 메서드가 처리하도록 했고, 이 메서드는 다시 mediaSourceCopier 라는 객체의 copy() 메서드에 위임하도록 코드를 작성했다. 아직 mediaSourceCopier의 정확한 구현은 모르지만, 단지 jobId에 해당하는 Job과 관련된 미디어 원본 파일을 로컬 파일 시스템에 복사한 뒤에 복사된 그 파일을 리턴해주는 역할을 mediaSourceCopier 객체에 부여했다. 아! 아직 Job 클래스는 출현하지 않았다.


컴파일 오류 때문에 위 테스트를 수행할 수 없다. 테스트를 통과시키기 위해 Mocking을 할 것이다. 우선, mediaSourceCopier 필드를 추가한다. (이클립스라면 빨간줄에서 Ctrl+1을 눌러서 빠르게 필드를 추가할 수 있다.) 그리고, 그 필드의 타입으로 MediaSourceCopier를 입력한다.


public class TranscodingServiceTest {


    private MediaSourceCopier mediaSourceCopier; // MediaSourceCopier 타입 없음


    @Test

    public void transcodeSuccessfully() {

        // 미디어 원본으로부터 파일을 로컬에 복사한다.

        Long jobId = new Long(1);

        File multimediaFile = copyMultimediaSourceToLocal(jobId);


        // 로컬에 복사된 파일을 변환처리한다.

        // 로컬에 복사된 파일로부터 이미지를 추출한다.

        // 변환된 결과 파일과 썸네일 이미지를 목적지에 저장

        // 결과를 통지

    }


    private File copyMultimediaSourceToLocal(Long jobId) {

        return mediaSourceCopier.copy(jobId); // copy 메서드 없음

    }

}


위 코드가 마저 컴파일 되도록 MediaSourceCopier 인터페이스를 추가하고, 그 인터페이스 copy() 메서드를 만들어 넣자.

public interface MediaSourceCopier {

    File copy(Long jobId);

}

이제 TranscodingServiceTest 클래스가 정상적으로 컴파일 된다. 단위 테스트를 실행해보자. 이런! NullPointerException이다. TranscodingServiceTest의 mediaSourceCopier 필드에 어떤 객체도 할당하지 않았기 때문에 Null 뻑이 났다.

아직 TranscodingService를 만들지도 않았는데, MediaSourceCopier의 구현체를 먼저 만들고 싶진 않다. 지금은 TranscodingService 구현을 만드는데 집중해야 한다. 그래서 MediaSourceCopier는 Mock 객체로 대체해서 위 테스트를 통과시킬 것이다. 필자가 좋아하는 Mock 라이브러리는 Mockito이므로 이를 이용해서 아래와 같이 Mock 객체를 생성했다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceTest {

    @Mock
    private MediaSourceCopier mediaSourceCopier;

    @Test
    public void transcodeSuccessfully() {
        Long jobId = new Long(1);
        File mockMultimediaFile = mock(File.class);
        when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);
        // 미디어 원본으로부터 파일을 로컬에 복사한다.
        File multimediaFile = copyMultimediaSourceToLocal(jobId);

        // 로컬에 복사된 파일을 변환처리한다.
        // 로컬에 복사된 파일로부터 이미지를 추출한다.
        // 변환된 결과 파일과 썸네일 이미지를 목적지에 저장
        // 결과를 통지
        verify(mediaSourceCopier, only()).copy(jobId);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        return mediaSourceCopier.copy(jobId);
    }
}

이제 테스트 코드를 실행해 보자. 녹색바! 통과다. 통과되었으므로, 불필요한 주석은 제거한다. (위 코드에서 취소 선을 그은 주석)

위와 비슷하게 로컬에 복사된 파일을 변환처리하기 위한 테스트 코드를 아래와 같이 추가하였다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceTest {

    @Mock
    private MediaSourceCopier mediaSourceCopier;
    @Mock
    private Transcoder transcoder;

    @Test
    public void transcodeSuccessfully() {
        Long jobId = new Long(1);
        File mockMultimediaFile = mock(File.class);
        when(mediaSourceCopier.copy(jobId)).thenReturn(mockMultimediaFile);

        List<File> mockMultimediaFiles = new ArrayList<File>();
        when(transcoder.transcode(mockMultimediaFile, jobId)).thenReturn(
                mockMultimediaFiles);
        
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        // 로컬에 복사된 파일을 변환처리한다.
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        // 로컬에 복사된 파일로부터 이미지를 추출한다.
        // 변환된 결과 파일과 썸네일 이미지를 목적지에 저장
        // 결과를 통지
        verify(mediaSourceCopier, only()).copy(jobId);
        verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        return mediaSourceCopier.copy(jobId);
    }

    private List<File> transcode(File multimediaFile, Long jobId) {
        return transcoder.transcode(multimediaFile, jobId);
    }
}

Transcoder 인터페이스도 알맞게 작성해 준다. 위 코드를 테스트하면 녹색바가 나올 것이다.

이런 과정을 반복적으로 거치면 최종적으로 아래와 같은 테스트 코드가 만들어진다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceTest {

    @Mock
    private MediaSourceCopier mediaSourceCopier;
    @Mock
    private Transcoder transcoder;
    @Mock
    private ThumbnailExtractor thumbnailExtractor;
    @Mock
    private CreatedFileSender createdFileSender;
    @Mock
    private JobResultNotifier jobResultNotifier;
    
    @Test
    public void transcodeSuccessfully() {
        Long jobId = new Long(1);
        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);
                
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThubmail(multimediaFile, jobId);
        sendCreatedFilesToDestination(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);
        
        verify(mediaSourceCopier, only()).copy(jobId);
        verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
        verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
        verify(createdFileSender, only()).send(mockMultimediaFiles,
                mockThumbnails, jobId);
        verify(jobResultNotifier, only()).notifyToRequester(jobId);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        return mediaSourceCopier.copy(jobId);
    }

    private List<File> transcode(File multimediaFile, Long jobId) {
        return transcoder.transcode(multimediaFile, jobId);
    }    

    private List<File> extractThubmail(File multimediaFile, Long jobId) {
        return thumbnailExtractor.extract(multimediaFile, jobId);
    }

    private void sendCreatedFilesToDestination(List<File> multimediaFiles,
            List<File> thumbnails, Long jobId) {
        createdFileSender.send(multimediaFiles, thumbnails, jobId);
    }

    private void notifyJobResultToRequester(Long jobId) {
        jobResultNotifier.notifyToRequester(jobId);
    }
}


테스트 코드로부터 TranscodingService 클래스 도출하기

TranscodingService 클래스와 관련된 거의 모든 코드가 출현했다. 이제 남은 작업은 TranscodingService를 도출하는 것이다. 앞서 코드에서 굵은 색으로 표시한 부분을 transcode()라는 이름을 갖는 메서드로 분리해보자. 그러면, 아래와 같이 테스트 코드가 바뀐다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceTest {

    ... // Mock 객체들

    @Test
    public void transcodeSuccessfully() {
        Long jobId = new Long(1);
        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);
                
        transcode(jobId);
        
        verify(mediaSourceCopier, only()).copy(jobId);
        verify(transcoder, only()).transcode(mockMultimediaFile, jobId);
        verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);
        verify(createdFileSender, only()).send(mockMultimediaFiles,
                mockThumbnails, jobId);
        verify(jobResultNotifier, only()).notifyToRequester(jobId);
    }
    
    private void transcode(Long jobId) {
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThubmail(multimediaFile, jobId);
        sendCreatedFilesToDestination(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);    
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        return mediaSourceCopier.copy(jobId);
    }
    ... // 나머지 메서드
}

코드를 바꿨으니 테스트 코드를 실행해서 녹색바가 보이는지 확인한다.

이제 조금만 더 하면 된다. 대망의 TranscodingService 클래스를 만들 차례이다. 다음과 같은 작업을 하면 된다.
  1. TranscodingService 클래스를 만든다.
  2. transcode() 메서드 및 그 메서드에서 사용하는 메서드를 TranscodingService 클래스로 옮긴다.
  3. transcode() 메서드에서 필요로 하는 협업 객체를 필드로 정의한다.
  4. 생성자를 이용해서 또는 Setter를 이용해서 협업 객체를 초기화한다.

결과로 만들어진 TranscodingService 클래스는 아래와 같다.


public class TranscodingService {

    private MediaSourceCopier mediaSourceCopier;

    private Transcoder transcoder;

    private ThumbnailExtractor thumbnailExtractor;

    private CreatedFileSender createdFileSender;

    private JobResultNotifier jobResultNotifier;


    public TranscodingService(MediaSourceCopier mediaSourceCopier,

            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,

            CreatedFileSender createdFileSender,

            JobResultNotifier jobResultNotifier) {

        this.mediaSourceCopier = mediaSourceCopier;

        this.transcoder = transcoder;

        this.thumbnailExtractor = thumbnailExtractor;

        this.createdFileSender = createdFileSender;

        this.jobResultNotifier = jobResultNotifier;

    }


    public void transcode(Long jobId) {

        File multimediaFile = copyMultimediaSourceToLocal(jobId);

        List<File> multimediaFiles = transcode(multimediaFile, jobId);

        List<File> thumbnails = extractThubmail(multimediaFile, jobId);

        sendCreatedFilesToDestination(multimediaFiles, thumbnails, jobId);

        notifyJobResultToRequester(jobId);

    }


    private File copyMultimediaSourceToLocal(Long jobId) {

        return mediaSourceCopier.copy(jobId);

    }


    private List<File> transcode(File multimediaFile, Long jobId) {

        return transcoder.transcode(multimediaFile, jobId);

    }


    private List<File> extractThubmail(File multimediaFile, Long jobId) {

        return thumbnailExtractor.extract(multimediaFile, jobId);

    }


    private void sendCreatedFilesToDestination(List<File> multimediaFiles,

            List<File> thumbnails, Long jobId) {

        createdFileSender.send(multimediaFiles, thumbnails, jobId);

    }


    private void notifyJobResultToRequester(Long jobId) {

        jobResultNotifier.notifyToRequester(jobId);

    }


}


이제 다음으로 할 작업은 테스트 클래스가 TranscodingService 클래스를 사용하도록 변경할 차례이다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {


    private Long jobId = new Long(1);


    @Mock

    private MediaSourceCopier mediaSourceCopier;

    @Mock

    private Transcoder transcoder;

    @Mock

    private ThumbnailExtractor thumbnailExtractor;

    @Mock

    private CreatedFileSender createdFileSender;

    @Mock

    private JobResultNotifier jobResultNotifier;


    private TranscodingService transcodingService;


    @Before

    public void setup() {

        transcodingService = new TranscodingService(mediaSourceCopier,

                transcoder, thumbnailExtractor, createdFileSender,

                jobResultNotifier);

    }


    @Test

    public void transcodeSuccessfully() {

        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);


        transcodingService.transcode(jobId);


        verify(mediaSourceCopier, only()).copy(jobId);

        verify(transcoder, only()).transcode(mockMultimediaFile, jobId);

        verify(thumbnailExtractor, only()).extract(mockMultimediaFile, jobId);

        verify(createdFileSender, only()).send(mockMultimediaFiles,

                mockThumbnails, jobId);

        verify(jobResultNotifier, only()).notifyToRequester(jobId);

    }


}


뭔가 바꿨으니 테스트를 실행해 보자. 녹색! 오케이!


이제 진짜 하나 남았다. TranscodingService를 TranscodingServiceImpl 클래스로 바꾸고, TranscodingService 인터페이스를 만들자. 그리고, TranscodingServceImpl 클래스가 TranscodingService 인터페이스를 상속받도록 하자. 만들어진 TranscodingService 인터페이스는 다음과 같다.


public interface TranscodingService {


    public void transcode(Long jobId);


}


기존의 TranscodingService 클래스는 이름을 TranscodingServiceImpl로 변경하고 TranscodingService 인터페이스를 상속받도록 구현한다.


public class TranscodingServiceImpl implements TranscodingService {

    private MediaSourceCopier mediaSourceCopier;

    private Transcoder transcoder;

    private ThumbnailExtractor thumbnailExtractor;

    private CreatedFileSender createdFileSender;

    private JobResultNotifier jobResultNotifier;


    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,

            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,

            CreatedFileSender createdFileSender,

            JobResultNotifier jobResultNotifier) {

        ...

    }


    @Override

    public void transcode(Long jobId) {

        ...

    }

    ...

}


테스트 코드는 이제 TranscodingServceImpl 클래스를 사용하도록 변경하면 끝!


결과는?


결과는 다음과 같다.

  1. 테스트로부터 시작해서 TranscodingService 객체가 협업해야 하는 다른 객체들의 인터페이스와 기능을 점진적으로 식별했다.
  2. 아직 정상적으로 실행되는 경우에 대해서만 TranscodingService가 구현되어 있다. 중간에 일부가 실패하는 경우에 대한 TranscodingService 구현이 필요하다. 이걸 단위 테스트로 한 번 만들어 볼 것이다.
  3. 최초에 설계했던 Job 클래스나 JobRepository 등은 아직 출현하지 않았다. 이 타입들은 앞으로 구현을 진행하면서 점진적으로 출현할 것 같다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 백명석 2012.10.24 10:45 신고  댓글주소  수정/삭제  댓글쓰기

    오호 ! Good job !!
    그리고 다음과 같은 생각도 듦.
    이런 테스트는 controller, service, repository에 적합할 듯. 근데 이 놈들에게는 로직이 거의 없어서 테스트의 필요가 적은 것이 좋을 듯.
    infra(http, html, jdbc 등)에 의존성이 없는 POJO에 중요한 로직을 위치시키고 이에 대한 테스트를 heavy하게 하는 것이 좋은 방법일 듯.

    • 최범균 madvirus 2012.10.24 11:14 신고  댓글주소  수정/삭제

      저도 공감합니다.
      그리고, 이건 연습 2부에서 실패 상황이 출현하면서 조금 더 테스트가 쓸모 있어 질 예정이에요. (아주 약간의 로직이 들어갈 것도 같구요.)