TDD에 대하여..

윤태호·2023년 4월 9일
0

TDD에 대한 생각!

  • 개발을 시작하고 공부를 하다보면 DDD,TDD 등 다양한 개발 방법론에 대해 듣게 된다. 하지만 필자처럼 개발 경력이 별로 없거나 많은 경험이 없는 주니어들은 실제로 TDD로 개발하기가 어려운 것 같다.(개인적인 필자의 생각)
  • 왜냐하면 서비스 로직 및 DTO, Entity, Query 작성 등에 익숙하지 않다는 핑계로 Test는 후순위에 두게 된다.

Test의 중요성을 느끼다.

필자도 Project를 하면서 TDD를 적용하지 못했습니다. Controller, Service, Repository 개발을 다 끝내고 Test를 위한 코드를 짜기 시작했습니다.
Test 코드도 단순히 Param을 넣어주면서 System out println으로 제대로 동작하는지 확인하는 정말 최악의 Test code를 짰습니다.
예시를 보여드리겠습니다.

  @Test
    @DisplayName("LikesOrder Test")
    public void OrderLikesTest() {
        List response = problemService.NotIncludeSolvedSort("likes", null, null, null, null, null, null, null, null);
        System.out.println(response);
    }

단순히 Junit 5에서 @SpringbootTest를 붙여서 매우 이상한 코드입니다.

Test Code에 눈을 뜨다.

Test Code를 도대체 어떻게 짜는 걸까??? TDD의 중요성이 생각나면서 도대체 머가 그렇게 중요하길래??라는 의문이 들면서 공부를 시작했습니다.

단위테스트와 통합테스트
MVC 모델의 프로젝트면 Controller, Service, Repository의 단위별 테스트가 존재할 수 있으며, 전체적인 통합테스트가 존재할 수 있습니다.

  • 통합테스트
    @SpringbootTest
    스프링 부트에서 의존성으로 전반적인 Application의 통합테스트를 진행할 수 있도록 제공합니다. 단 Junit 버전에 따라 달라질 수 있으니 참고해야합니다.
  • 단위테스트
    @DataJpaTest
    @Mock
    @WebMvc
    각 계층마다 단위테스트를 할 수 있는 여러가지 Annotation을 제공합니다.

그럼 단위테스트와 통합테스트 무엇을 해야할까??

결론 둘 다 해야하지만, TDD의 개발 방향은 단위테스트를 진행하면서 개발을 진행해 나가는 것으로라고 생각합니다. 왜냐하면 통합테스트는 만약 에러가 나면 어디 계층에서 Error가 났는지 찾기도 힘들고, Refactoring도 힘듭니다. 그리고 추가적으로 통합테스트는 가짜 객체를 통해 Test하는 것이 아니라 실제 DB에 기록하기에 이런 단점도 있습니다!!
그래서 단위테스트 -> 외부적 환경까지 고려한 통합테스트를 진행하면 된다고 생각합니다.

단위테스트를 진행하면서 느낀 것!!

필자는 Mockito를 활용하면서 단위테스트를 진행하였습니다. @Mock 객체로 가짜 객체를 주입 받아 진행하였습니다. 필자가 작성했던 예시 코드를 아래에 보여드리겠습니다.

@ExtendWith(MockitoExtension.class)
public class ProblemServiceTest {
    @InjectMocks
    private SolutionServiceImp solutionService;
    @InjectMocks
    private ProblemServiceImpl problemService;
    @Mock
    ProblemRepository problemRepository;
    @Mock
    SolutionRepository solutionRepository;
    @Mock
    AttachedRepository attachedRepository;
    @Nested
    @DisplayName("문제 생성")
    class CreateProblem{
        private String type;
        private String writer;
        private String title;
        private String content;
        private String answer;
        private String level;
        private String hashtag;
        @BeforeEach
        void setup(){
            type = "answer";
            writer = "Taeho";
            title = "Project";
            content = "Test CODE 끝내고 싶어요";
            answer = "빨리끝내고 React 시작해야함";
            level = "gold";
            hashtag = "CS,BackEnd";
        }
        @Nested
        @DisplayName("정상 케이스")
        class SuccessCase {
            @Test
            @DisplayName("새로운 문제 작성")
            void createProblemSuccessCase(){
                ProblemSaveRequestDto problemSaveRequestDto = ProblemSaveRequestDto.builder()
                        .type(type)
                        .level(level)
                        .title(title)
                        .writer(writer)
                        .answer(answer)
                        .content(content)
                        .tag(hashtag)
                        .content(content)
                        .build();
                Problem problem = Problem.builder().
                        title(title).
                        writer(writer).
                        content(content).
                        answer(answer).
                        type(type).
                        level(level).
                        hashtag(hashtag)
                        .build();
   when(problemRepository.save(any(Problem.class))).thenReturn(problem);
                ProblemSaveResponseDto result = problemService.save(problemSaveRequestDto);
                assertThat(result.getData().getWriter()).isEqualTo("Taeho");
            }
        }
        @Nested
        @DisplayName("실패한 케이스")
        class FailCase{
            @Test
            @DisplayName("반환된 게시물이 Null일 경우")
            void FailCase1(){
                ProblemSaveRequestDto problemSaveRequestDto = ProblemSaveRequestDto.builder()
                        .type(type)
                        .level(level)
                        .title(title)
                        .writer(writer)
                        .answer(answer)
                        .content(content)
                        .tag(hashtag)
                        .content(content)
                        .build();
                when(problemRepository.save(any(Problem.class))).thenReturn(null);
                ProblemSaveResponseDto result = problemService.save(problemSaveRequestDto);
                assertThat(result.getData()).isNull();
            }
        }
    }

필자는 @Nested 와 @BeforeEach 등의 Annotation을 활용하면서 최대한 실제 Service를 개발하는 방향과 유사하게 Test Code를 작성해 나갔습니다.추가적으로 예외가 발생하는 경우에 대한 Test를 진행해 나갔습니다.

위처럼 Test Code를 진행해 나가면서 필자는 Code 리팩토링이 매우 많이 이뤄졌습니다. 그 이유는 아래와 같습니다.

  • Clean 하지 못한 Code
  • 예외처리를 안 한 경우들을 발견

추가적으로 Mockito는 매우 엄격한 규칙을 추구하기에 클린한 코드를 작성하게 끔 합니다. 예를 들어 사용하지 않는 메소드나,Return값들의 코드가 있다면 Stubbing strict 에러와 함께 Test가 돌아가지 않습니다. 이처럼 TDD를 통해서 에러 및 예외처리를 검증함과 동시에 Clean한 코드를 만들 수 있습니다.

글을 마치며

Test code 작성법을 알려드리는 글은 아니였습니다. 하지만 TDD의 중요성을 필자가 겪고 작성했던 글이였습니다. 다들 TDD 하시길 바랍니당 ㅎㅎ
아 그리고 TDD를 진행하면서 예외처리를 좀 더 클린하게 바꾸면서 클린코드를 작성하도록 했는데, 예외처리를 어떻게 했는지 다음 글을 쓰도록 하겠습니다!

profile
성장하는것을 제일 즐깁니다.

0개의 댓글