주요글: 도커 시작하기
반응형

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


실패 상황 시나리오 구현하기


앞서 http://javacan.tistory.com/entry/TDD-exercise-1-Design-communication-using-tdd 글에 이어 실패 상황을 구현해 보자. 우선 원본 소스로부터 파일을 복사해 오는 과정이 실패났을 경우를 가정해서 테스트 코드를 만든다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {

    private Long jobId = new Long(1);


    @Mock

    private MediaSourceCopier mediaSourceCopier;

    ....

    private TranscodingService transcodingService;


    @Test

    public void transcodeSuccessfully() {

        ....

    }


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.isLastState());


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

        verify(transcoder, never()).transcode(any(File.class), anyLong());

        verify(thumbnailExtractor, never()).extract(any(File.class), anyLong());

        verify(createdFileSender, never()).store(anyListOf(File.class),

                anyListOf(File.class), anyLong());

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

    }

}


위 테스트 메서드는 MediaSourceCopier가 copy 과정에서 문제가 발생한 경우 그걸 검증하는 코드로 작성되었다. 먼저 transcodingService.transcode() 메서드는 익셉션을 발생시켜야 한다. 그렇지 않을 경우 테스트가 실패하도록 했다. 그리고, mediaSourceCopier를 제외한 나머지 협업 객체는 호출되어서는 안 되므로, 호출 검증 값을 never()로 지정하였다.


드디어 Job과 jobRepository가 출현했다. 먼저 연습 1에서 했던 것 처럼, jobRepository를 필드로 만들고 JobRepository 인터페이스를 생성한다. 그리고, Job 클래스도 함께 만들어준다.


@RunWith(MockitoJUnitRunner.class)

public class TranscodingServiceImplTest {

    ...

    @Mock

    private JobRepository jobRepository;

    

    private TranscodingService transcodingService;

    ...

    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ...

    }

}


컴파일 에러다. 컴파일 에러가 나지 않도록 다음과 같이 메서드/enum 타입 등을 추가해 주자.


public interface JobRepository {

    Job findById(Long jobId);

}



public class Job {


    public static enum State {

        MEDIASOURCECOPYING

    }


    public boolean isSuccess() {

        return false;

    }


    public State getLastState() {

        return null;

    }


}


컴파일 에러를 제거했으므로 테스트를 실행한다. 테스트가 실패한다. 원인은 다음과 같다.


java.lang.AssertionError: 발생해야 함

at org.junit.Assert.fail(Assert.java:91)

at org.chimi.s4t.domain.transcode.TranscodingServiceImplTest.transcodeFailBecauseExceptionOccuredAtMediaSourceCopier(TranscodingServiceImplTest.java:74)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

        ...


아! transcodingService.transcode() 메서드가 익셉션을 발생시켜야 하는데, 무사히 실행되어서 fail()에 걸렸다. 이 테스트는 원본 미디어 파일을 복사해 오는 도중에 에러가 발생한 경우를 가정한 것이므로 mediaSourceCopier Mock 객체가 익셉션을 발생시키도록 하자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isDone());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ...

    }

    



다시 테스트 실행.... 빨간색이다. NullPointerException이다. 스택트레이스를 보니 아래 줄에서 job이 null이어서 발생한 것이다.


assertFalse(job.isSuccess());


jobRepository가 Job 객체를 리턴하도록 Mock 객체를 설정하자.


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());



다시 테스트 실행. 에러가 났다. job.getLastState()가 MEDIASOURCECOPYING이 아니라는 에러다. 그런데, job.isSuccess()가 false인지 확인하는 코드는 통과되었다. 이런! 잠시 고민하다가, 앞에서 작성했었던 transcodeSuccessfully() 테스트에 다음과 같이 Job에 대한 검증 코드를 추가하였다.



    @Test

    public void transcodeSuccessfully() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        ...

        transcodingService.transcode(jobId);


        Job job = jobRepository.findById(jobId);

        assertTrue(job.isSuccess());

        assertEquals(Job.State.COMPLETED, job.getLastState());

        ... // Mock 객체들의 상호작용 확인

    }


    @Test

    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {

        when(jobRepository.findById(jobId)).thenReturn(new Job());

        

        RuntimeException mockException = new RuntimeException();

        when(mediaSourceCopier.copy(jobId)).thenThrow(mockException);

        

        try {

            transcodingService.transcode(jobId);

            fail("발생해야 함");

        } catch (Exception ex) {

            assertSame(mockException, ex);

        }


        Job job = jobRepository.findById(jobId);

        assertFalse(job.isSuccess());

        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

        ... // Mock 객체들의 상호작용 확인

    }


Job.State enum 타입에 COMPLETED를 추가한 뒤에 다시 테스트를 실행해 보자. 에러다! 두 테스트 메서드에서 모두 실패가 발생했다.

  • transcodeSuccessfully() 메서드의 실패 지점
    • assertTrue(job.isSuccess()) 
    • 아직 assertEquals(Job.State.COMPLETED, job.getLastState()) 는 확인도 못한다.
  • transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 메서드의 실패 지점
    • assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());

이제 이 두 테스트 메서드가 모두 통과되도록 TranscodingServiceImpl을 수정해 보자.


TranscodingServiceImpl 구현 고민 그리고 새로운 협업 객체 등장


테스트를 통과시키기 위해서 TranscodingServiceImpl 클래스에 다음의 처리를 해야 한다.

  • 성공적으로 실행되면 Job의 상태를 COMPLETED로 바꾸고 성공 여부를 true로 지정한다.
  • 원본 파일 복사 전에 Job의 상태를 MEDIASOURCECOPYING로 바꾸고, 복사 과정 중 익셉션이 발생하면 Job의 성공 여부를 false로 지정한다.
위의 내용을 보면 뭔가 Job의 상태를 변경해주는 기능이 필요해 보인다. 또한, 익셉션이 발생하면 에러 상태로 변경해 주어야 할 것 같기도 하다.

우선, TranscodingServceImpl 클래스에 상태를 변경해주는 기능을 아래와 같이 별도의 메서드인 changeJobState()로 추상화하고, transcode()는 그 메서드를 호출하도록 했다.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThumbnail(multimediaFile, jobId);
        storeCreatedFilesToStorage(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);
        changeJobState(jobId, Job.State.COMPLETED);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState); // 컴파일 에러
    }


위 코드에서 changeJobState() 메서드는 Job의 상태 변경 처리를 jobStateChanger 객체에 위임하고 있다. (이런 위임은 이미 copyMultimediaSourceToLocal() 메서드나 extractThumbnail() 메서드 등에서 동일하게 사용하고 있으며, 위임을 하면 Mock 객체를 사용해서 빠르게 테스트를 통과시킬 수 있게 된다.) jobStateChange 필드가 없어서 컴파일 에러가 나므로, jobStateChanger 필드를 추가하고 JobStateChanger 인터페이스를 만들고, 그 인터페이스를 changeJobState() 메서드를 정의해서 컴파일 에러를 제거한다. 또한, 생성자를 통해서 JobStateChanger 객체를 전달받도록 수정하자.

public class TranscodingServiceImpl implements TranscodingService {
    private MediaSourceCopier mediaSourceCopier;
    private Transcoder transcoder;
    private ThumbnailExtractor thumbnailExtractor;
    private CreatedFileSaver createdFileSaver;
    private JobResultNotifier jobResultNotifier;
    private JobStateChanger jobStateChanger;

    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,
            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier, JobStateChanger jobStateChanger) {
        ...
        this.jobStateChanger = jobStateChanger;
    }

    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        List<File> multimediaFiles = transcode(multimediaFile, jobId);
        List<File> thumbnails = extractThumbnail(multimediaFile, jobId);
        storeCreatedFilesToStorage(multimediaFiles, thumbnails, jobId);
        notifyJobResultToRequester(jobId);
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

생성자가 수정되었으므로, 기존에 생성자를 사용한 테스트 코드에서 컴파일 에러가 발생한다. 컴파일 에러가 발생하지 않도록 JobStateChanger에 대한 Mock 객체를 만들고, 생성자에 전달해주도록 하자.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private JobRepository jobRepository;
    @Mock
    private JobStateChanger jobStateChanger;

    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger);
    }

새로운 협업 객체가 생겼다. 이제 첫 번째로 할 작업은 정상적으로 실행된 경우의 테스트를 통과시키는 것이다. 이를 위해 해야 할 작업은 JobStateChanger의 Mock 객체가 알맞게 Job의 상태를 변경시키도록 하는 것이다. 아래 코드는 테스트를 통과시키기 위해 Mock 객체에 기능을 부여한 코드이다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private JobStateChanger jobStateChanger;

    private Job mockJob = new Job();
    
    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger);

        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() {
        when(jobRepository.findById(jobId)).thenReturn(mockJob);
        ...
        Job job = jobRepository.findById(jobId);
        assertTrue(job.isSuccess());
        assertEquals(Job.State.COMPLETED, job.getLastState());
        ...
    }

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        when(jobRepository.findById(jobId)).thenReturn(mockJob);
        ...
        Job job = jobRepository.findById(jobId);
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        ...
    }
}

위 코드에서 주의할 점은 Job 객체를 mockJob으로 필드로 빼고, 각 테스트 메서드에서 jobRepository Mock 객체의 findById(jobId) 메서드가 호출되면 mockJob을 리턴하도록 구현했다는 것이다.

Job 클래스에 changeState() 메서드가 없어서 컴파일 에러가 발생하고 있으므로, changeState() 메서드를 구현할 차례이다. 이 메서드를 알맞게 구현해서 위 테스트가 통과되도록 만들면 된다. 통과시키려면 다음과 같이 changeState() 메서드를 구현해 주면 될 것 같다.

public class Job {

    public static enum State {
        MEDIASOURCECOPYING, COMPLETED
    }

    private State state;

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    public State getLastState() {
        return state;
    }

    public void changeState(State newState) {
        this.state = newState;
    }

}

이제 테스트를 실행해 보자. 오, 예! 녹색바다.

실패의 의미가 없다.

미디어 원본 파일을 복사하는 것 뿐만 아니라 변환 처리, 썸네일 추출, 결과 전송, 통지에 대해서도 동일하게 실패 과정에 대한 테스트를 넣으려고 했으나, 마음에 안 드는 부분을 발견했다. transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 메서드의 Job에 대한 검증 부분을 다시 살펴보자.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        Job job = jobRepository.findById(jobId);
        assertFalse(job.isSuccess());
        assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
        ...
    }

job.isSuccess()는 작업의 성공 여부는 알려주는데, 작업이 중간에 실패했는지 아니면 작업이 진행중인지의 여부는 알려주지 못한다. 그렇다, 우리에겐 다음이 필요하다.
  • 작업이 시작 전 / 진행 중 / 끝남 여부
  • 작업의 성공/실패 여부
    • 실패했다면, 어느 단계에서 실패했는 지 그리고 실패 원인
몇 가지 assert를 떠올려 봤는데, 아래와 같이 하면 좋을 것 같다.

    @Test
    public void transcodeSuccessfully() {
        ...
        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 들
    }

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        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.MEDIASOURCECOPYING, job.getLastState());
        assertNotNull(job.getOccurredException());
        ... // verify 들
    }

추가된 메서드는 다음과 같다.
  • Job.isWaiting(): 작업이 대기 중인지의 여부
  • Job.isFinished(): 작업이 완료되었는지의 여부
  • Job.getOccuredException(): 진행 과정 중 발생한 익셉션을 구한다.
Job.isSuccess()는 작업이 성공/실패 여부를 의미하도록 변경했다. 컴파일 에러가 발생하므로, 각각의 메서드를 추가해 주고 점진적으로 테스트를 통과시켜 나갈 차례이다. transcodeSuccessfully() 테스트가 통과될 때 까지 테스트 실행/Job 클래스 수정을 반복해서 Job 클래스를 다음과 같이 수정하였다.

public class Job {

    public static enum State {
        MEDIASOURCECOPYING, COMPLETED
    }

    private State state;
    private Exception occurredException;

    public void changeState(State newState) {
        this.state = newState;
    }

    public boolean isWaiting() {
        return state == null;
    }

    public boolean isFinished() {
        return isSuccess() || isExceptionOccurred();
    }

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    private boolean isExceptionOccurred() {
        return occurredException != null;
    }

    public State getLastState() {
        return state;
    }

    public Exception getOccurredException() {
        return occurredException;
    }

}

이제 남은 작업은 transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() 테스트를 통과시킬 차례이다. 실패 케이스의 테스트를 실행해보면 아래 코드를 통과하지 못한다.

assertTrue(job.isFinished()); // 통과 실패
assertFalse(job.isSuccess());
assertEquals(Job.State.MEDIASOURCECOPYING, job.getLastState());
assertNotNull(job.getOccurredException());

job.isFinished() 메서드는 작업이 성공했거나 익셉션이 발생한 경우 true를 리턴하도록 구현하였다. 따라서, 필요한 것은 TranscodingServiceImpl이 처리 도중 익셉션이 발생하면 그 사실을 Job 객체에 알려주는 것이다. 뭔가 Job 객체에게 알려주는 기능이 필요해졌다. 음,, 지금까지 글을 따라 읽어왔다면 이 쯤에서 새로운 협업 객체가 등장할 것 같은 느낌을 받을 것이다.

익셉션 발생을 Job에 알리기 위한 새로운 협업 객체의 등장

각 처리 과정에서 익셉션이 발생하면 그 사실을 Job에게 알려주는 기능을 추가하자. 약간의 고민을 한 끝에 아래와 같이 해 보기로 결정했다.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        ...
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        try {
            return mediaSourceCopier.copy(jobId);
        } catch (RuntimeException ex) {
            transcodingExceptionHandler.notifyToJob(jobId, ex);
            throw ex;
        }
    }
    ... // 나머지 메서드도 동일하게 transcodingExceptionHandler 사용 코드 추가

transcodingExceptionHandler라는 새로운 협업 객체가 등장했다. 이 객체의 notifyToJob() 메서드는 Job 객체에게 익셉션이 발생했다는 사실을 알린다. 그리고 익셉션이 사라지지 않도록 재전파한다.

transcodingExceptionHandler 협업 객체가 없으므로, 필드로 정의하고 transcodingExceptionHandler 객체를 위한 인터페이스를 생성하고, 그 인터페이스에 notifyToJob() 메서드를 정의하자. 물론, 생성자를 통해서 추가된 협업 객체를 전달받도록 수정하는 것도 잊지 말자.

public class TranscodingServiceImpl implements TranscodingService {
    ...
    private JobStateChanger jobStateChanger;
    private TranscodingExceptionHandler transcodingExceptionHandler;

    public TranscodingServiceImpl(MediaSourceCopier mediaSourceCopier,
            Transcoder transcoder, ThumbnailExtractor thumbnailExtractor,
            CreatedFileSaver createdFileSaver,
            JobResultNotifier jobResultNotifier,
            JobStateChanger jobStateChanger,
            TranscodingExceptionHandler transcodingExceptionHandler) {
        ...
        this.transcodingExceptionHandler = transcodingExceptionHandler;
    }

    @Override
    public void transcode(Long jobId) {
        changeJobState(jobId, Job.State.MEDIASOURCECOPYING);
        File multimediaFile = copyMultimediaSourceToLocal(jobId);
        ...
        changeJobState(jobId, Job.State.COMPLETED);
    }

    private void changeJobState(Long jobId, State newJobState) {
        jobStateChanger.chageJobState(jobId, newJobState);
    }

    private File copyMultimediaSourceToLocal(Long jobId) {
        try {
            return mediaSourceCopier.copy(jobId);
        } catch (RuntimeException ex) {
            transcodingExceptionHandler.notifyToJob(jobId, ex);
            throw ex;
        }
    }

TranscodingServiceImpl의 생성자를 변경했으므로, 테스트 코드도 함께 수정해 준다.

@RunWith(MockitoJUnitRunner.class)
public class TranscodingServiceImplTest {
    ...
    @Mock
    private TranscodingExceptionHandler transcodingExceptionHandler;

    private Job mockJob = new Job();

    private TranscodingService transcodingService;

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger, transcodingExceptionHandler);
        ...
    }


앞서 실패했던 부분을 다시 보자.

    @Test
    public void transcodeFailBecauseExceptionOccuredAtMediaSourceCopier() {
        ...
        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.MEDIASOURCECOPYING, job.getLastState());
        assertNotNull(job.getOccurredException());
...

    }

이제 남은 작업은 새로 추가된 협업 객체를 이용해서 위 테스트과 통과되도록 만드는 것이다. 우선, 테스트 클래스에 새롭게 추가된 Mock 객체인 transcodingExceptionHandler의 notifyToJob() 메서드가 호출되면 mockJob객체에 발생한 익셉션을 전달할 수 있도록 exceptionOccured() 메서드를 호출한다.

    @Before
    public void setup() {
        transcodingService = new TranscodingServiceImpl(mediaSourceCopier,
                transcoder, thumbnailExtractor, createdFileSender,
                jobResultNotifier, jobStateChanger, transcodingExceptionHandler);

        ...

        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                RuntimeException ex = (RuntimeException) invocation.getArguments()[1];
                mockJob.exceptionOccurred(ex);
                return null;
            }
        }).when(transcodingExceptionHandler).notifyToJob(anyLong(),
                any(RuntimeException.class));
    }

물론, 아직 Job 클래스에 exceptionOccurred() 메서드는 없다. 이제부터 만들어야 한다. 이 메서드를 만드는 건 어렵지 않을 것 같다. 다음과 같이 이 메서드를 통해서 전달받은 익셉션 객체를 필드에 보관하기만 하면 된다.

package org.chimi.s4t.domain.job;

public class Job {
    ...
    public boolean isFinished() {
        return isSuccess() || isExceptionOccurred();
    }

    public boolean isSuccess() {
        return state == State.COMPLETED;
    }

    private boolean isExceptionOccurred() {
        return occurredException != null;
    }

    public Exception getOccurredException() {
        return occurredException;
    }

    public void exceptionOccurred(RuntimeException ex) {
        occurredException = ex;
    }

}

필요한 구현을 한 것 같으므로 테스트를 실행해 보자. 야호! 녹색바다!

이제 나머지 실패 시나리오에 대해서 추가적으로 테스트 클래스를 만들고 비슷한 과정을 반복하면 된다. 아, 잠깐! 비슷한 과정을 반복하다보면 각각의 테스트 메서드에 중복된 코드가 출현하게 된다. 중복은 악의 축이므로, 이를 없애야 한다. 테스트 코드도 유지보수가 가능하도록 리팩토링을 해 주어야 하는데, 이 과정은 연습 3에서 진행해 본다.


결과는?

  • 드디어 Job과 JobRepository가 출현했다.
  • 두 개의 협업 객체가 새롭게 추가되었다. (최초에 필자가 머리속으로 상상했던 것 보다 더 많은 협업 객체가 출현했다.)
  • 성공 시나리오와 실패 시나리오에 대한 테스트도 만들어졌다.

연습3에서는 앞서 말한데로 테스트 코드의 중복을 제거해 보도록 하자.

+ Recent posts