본문 바로가기

Proj/Tripot

스프링부트 테스트코드 작성하기: 통합 테스트

1. 개요

MVC 계층의 단위 테스트를 모두 작성하였으니 이번에는 통합 테스트를 작성해보고자 한다. 

2. 애노테이션

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = "spring.profiles.active=local")
@Transactional
@Import(SecurityConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class BaseIntegrationTest {

	...
}

 

위의 base class를 기반으로 각 클래스가 이를 상속받아 테스트를 진행한다. 각 애노테이션의 쓰임새를 알아보자.

  • @SpringBootTest: 애플리케이션 컨텍스트를 생성하고, 프로젝트의 모든 스프링 빈을 등록하여 테스트 환경을 구축한다.
  • @AuthConfigureMockMvc: MockMvc를 사용하기 위한 인스턴스가 자동으로 구성된다.  테스트 대상이 아닌 @Service나 @Repository가 붙은 객체들도 모두 메모리에 올린다.
  • @TestPropertySource(properties = "sping.profiles.active=local"): 테스트를 위한 설정 정보를 정의한다. 여기에서는 local 프로필을 적용하돌고 하였다.
  • @Transactional: 각 테스트가 끝나고 DB의 변경사항을 롤백한다.
  • @Import(SecurityConfig.class): 스프링 시큐리티의 설정 클래스를 import한다.
  • @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD): 각 테스트가 끝나고 context를 새로 생성한다.
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

 

컨트롤러 단위 테스트에서 사용했던 요청을 보내기 위한 MockMvc와 body를 json 형식으로 변환하기 위한 objectMapper를 사용한다.

 

이를 상속받아 여러 테스트를 진행했지만 그 중 일부를 작성하고자 한다.

3. 테스트 코드

3-1. MemberIntegrationTest.changeProfileImage()

    
    //aws 테스트는 요금이 발생할 수 있으므로 해당 객체를 mock 처리
    @MockBean
    private AmazonS3Client amazonS3Client;
    
    @BeforeEach
    void init() throws MalformedURLException {
        Member preactiveTestMember = createPreactiveTestMember();
        Member activeTestMember = createActiveTestMember();

        memberRepository.save(preactiveTestMember);
        memberRepository.save(activeTestMember);

        given(amazonS3Client.getUrl(any(), any())).willReturn(new URL("https://aws.com/new-url"));
    }

우선 테스트에 불필요한 비용이 드는 것을 방지하기 위해 amazonS3Client를 mock처리 해주고, 프로필 사진이 s3에 저장되었다 가정하고 그 주소를 리턴하도록 처리해주었다.

    @Test
    @WithMockCustomUser
    @DisplayName("프로필 사진 수정 응답이 정상적으로 반환되어야 함")
    void changeProfileImage() throws Exception {

        //given
        MockMultipartFile profileImg = createMockMultipartFile();

        //s3에 저장하는 과정이 생략되어야 함 -> AmazonS3Client mock 처리


        //when
        ResultActions actions = mockMvc.perform(
                multipart(HttpMethod.PATCH, "/api/v1/members/profile-images")
                        .file(profileImg)
                        .contentType(MediaType.MULTIPART_FORM_DATA)
                        .accept(MediaType.APPLICATION_JSON)
        );

        //then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.customCode").value("MEMBER-SUCCESS-006"))
                .andExpect(jsonPath("$.customMessage").value("회원 프로필 사진 변경 성공"))
                .andExpect(jsonPath("$.status").value(true))
                .andExpect(jsonPath("$.data").value(nullValue()));

        //회원 정보에 변경된 url이 저장되어야 함
        Member member = memberRepository.findById(2L).get();
        assertThat(member.getProfileImage()).isEqualTo("https://aws.com/new-url");

    }

요청을 보내는 방식은 컨트롤러 단위 테스트와 동일하게 mockMvc.perform()으로 보내줄 수 있다. 다만, 이번에는 service 및 이에 의존하는 클래스들이 mock이 아닌 실제 인스턴스로 동작하게 된다. 프로필 변경 후 의도하는 응답이 오는지, 회원 프로필사진 url이 정상적으로 변경되었는 지 체크하도록 작성했다. 

3-2 ReportIntegrationTest

    @BeforeEach
    void init() throws InterruptedException {
        Member preactiveTestMember = createPreactiveTestMember();
        Member activeTestMember = createActiveTestMember();
        Member testAdmin = createAdmin();
        Member activeTestMember2 = createActiveTestMember2();

        memberRepository.save(preactiveTestMember);
        memberRepository.save(activeTestMember);
        memberRepository.save(testAdmin);
        memberRepository.save(activeTestMember2);

        Story testStory = createStory(activeTestMember);
        storyRepository.save(testStory);

        Comment testComment = createComment(activeTestMember, testStory);
        commentRepository.save(testComment);

        for (int i = 1; i <= 100; i++) {
            Report testReport;

            if (i % 2 == 1) {

                testReport = Report.builder()
                        .reportType(ReportType.STORY)
                        .story(testStory)
                        .reportReason(ReportReason.SPAMMARKET)
                        .member(activeTestMember)
                        .build();


            } else {
                testReport = Report.builder()
                        .reportType(ReportType.COMMENT)
                        .comment(testComment)
                        .reportReason(ReportReason.SPAMMARKET)
                        .reportStatus(ReportStatus.CONFIRMED)
                        .member(activeTestMember)
                        .build();


            }

            Thread.sleep(5);

            reportRepository.save(testReport);
        }


    }

테스트 이전에 회원들을 저장하고, member1이 스토리, 댓글을 작성하고, 신고를 50번씩 진행하도록 했다. 페이징을 위한 데이터 저장이므로 몇 가지 제약조건은 무시했다.

    @Test
    @DisplayName("스토리에 대한 신고 기능이 정상적으로 이루어져야 함")
    @WithMockCustomUser2
    public void report_story() throws Exception {
        //given
        CreateReportDto createReportDto = new CreateReportDto(1L, "STORY", "스팸홍보");
        String content = objectMapper.writeValueAsString(createReportDto);

        //when
        ResultActions actions = mockMvc.perform(
                post("/api/v1/reports")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
        );


        //then
        actions
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.customCode").value("REPORT-SUCCESS-001"))
                .andExpect(jsonPath("$.customMessage").value("신고 성공"))
                .andExpect(jsonPath("$.status").value(true))
                .andExpect(jsonPath("$.data").value(nullValue()));

        //신고 내역이 정상적으로 저장되어야 함
        Report report = reportRepository.findById(101L).orElseThrow(RuntimeException::new);

        assertThat(report.getMember().getUsername()).isEqualTo("테스트사용자유저네임2");
        assertThat(report.getReportType()).isEqualTo(ReportType.STORY);
        assertThat(report.getReportReason()).isEqualTo(ReportReason.SPAMMARKET);
        assertThat(report.getStory().getTitle()).isEqualTo("testStoryTitle");
        assertThat(report.getComment()).isNull();
        assertThat(report.getReportStatus()).isEqualTo(ReportStatus.UNCONFIRMED);


    }

    @Test
    @DisplayName("본인 글은 신고할 수 없어야 함")
    @WithMockCustomUser
    public void report_story_equal_author() throws Exception {
        //given
        CreateReportDto createReportDto = new CreateReportDto(1L, "STORY", "스팸홍보");
        String content = objectMapper.writeValueAsString(createReportDto);

        //when
        ResultActions actions = mockMvc.perform(
                post("/api/v1/reports")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content)
        );


        //then
        actions
                .andDo(print())
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.customCode").value("REPORT-ERR-004"))
                .andExpect(jsonPath("$.customMessage").value("본인 글은 신고할 수 없음"))
                .andExpect(jsonPath("$.status").value(false))
                .andExpect(jsonPath("$.data").value(nullValue()));



    }

본인 글은 신고할 수 없고, member1이 스토리를 작성했으므로 member2가 신고하면 정상처리, member1이 신고하면 불가능하도록 처리하는 지 확인하는 테스트를 작성했다.

 

그 외 신고에 대한 처리, 신고 조회 등 정상적으로 작동되는지에 대한 테스트를 진행했다.

 

4. 후기

처음에는 테스트를 작성하는데 시간이 오래걸린다고 느꼈지만, 점점 익숙해지고 나니까 postman으로 직접 요청을 보내는 것보다 테스트가 편했다. 데커톡 1회에서 어떤 분이 테스트코드 작성 관련 질문을 했는데 강사분께서 "테스트코드가 훨씬 빠르고 편하다"는 뉘앙스의 답변을 해주셨는데 직접 짜보니 어떤 느낌인지 이해가 가게 되었다. 실제로 테스트코드를 작성하고 나서 postman은 거의 사용하지 않게 되었다.