테스트 코드에서의 @Transaction

이규훈·2023년 9월 21일

// 스프링 부트 테스트 환경에서 MockMvc를 자동으로 설정합니다.
@AutoConfigureMockMvc
// 스프링 부트의 테스트를 위한 어노테이션으로, 애플리케이션 컨텍스트를 로드하여 테스트를 진행합니다.
@SpringBootTest
// JUnit5를 사용할 때 테스트 설명을 위해 사용하는 어노테이션입니다.
@DisplayName("DataRest - API 테스트")
@Transactional
public class DataRestTest {


    // MockMvc는 웹 서버를 실행하지 않고도 스프링 MVC (Controller 계층)를 테스트할 수 있게 해주는 클래스입니다.
    private final MockMvc mvc;

    // 생성자 주입을 통해 MockMvc 인스턴스를 받아옵니다.
    public DataRestTest(@Autowired MockMvc mvc) {
        this.mvc = mvc;
    }



    @DisplayName("[api] 게시글 리스트 조회")
    @Test
    void givenNothing_whenRequestingArticles_thenReturnsArticlesJsonResponse() throws Exception {
        // given: 테스트 데이터 준비 및 초기 설정

        // when: 테스트할 기능을 수행
        mvc.perform(get("/api/articles"))
                .andExpect(status().isOk()) // HTTP 상태 코드가 200 (OK) 인지 검사
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json"))); // 반환된 컨텐츠 타입이 'application/hal+json'인지 검사
        // then: 결과 검증 (위에서 이미 검증하였기 때문에 별도로 작성하지 않음)
    }

    @DisplayName("[api] 게시글 단건 조회")
    @Test
    @Transactional  // 해당 어노테이션은 테스트 동작 중 발생한 DB의 변화를 테스트 종료 후 롤백합니다. 즉, 테스트에 의한 영향을 DB에 남기지 않게 합니다.
    void givenNothing_whenRequestingArticle_thenReturnsArticleJsonResponse() throws Exception {
        // given
        // MockMvc를 사용하여 게시글 생성 API를 호출합니다.
        // contentType은 전송하는 데이터의 타입을 지정하며, 여기서는 JSON 형식입니다.
        // content는 전송할 실제 데이터를 지정합니다.
        MvcResult result = mvc.perform(post("/api/articles")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"제목\",\"content\":\"내용\",\"hashtag\":\"해시태그\"}"))
                .andExpect(status().isCreated())  // 응답 상태가 '생성됨'(201)인지 확인합니다.
                .andReturn();  // 테스트의 결과를 MvcResult 객체에 저장하여 후속 처리나 검증에 사용합니다.

        // MvcResult는 MockMvc의 실행 결과를 나타내는 객체로, 응답의 상태, 데이터, 헤더 등의 정보를 포함합니다.
        String location = result.getResponse().getHeader("Location");  // 생성된 게시글의 URI를 Location 헤더에서 가져옵니다.
        String articleId = location.substring(location.lastIndexOf('/') + 1);  // Location URI에서 게시글의 ID를 추출합니다.

        // when
        // 추출한 게시글 ID를 사용하여 해당 게시글을 조회하는 API를 호출합니다.
        mvc.perform(get("/api/articles/" + articleId))
                .andExpect(status().isOk())  // 응답 상태가 '정상'(200)인지 확인합니다.
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));  // 응답의 컨텐츠 타입이 'application/hal+json'인지 확인합니다.
        // then
        // 이 부분에는 결과에 대한 추가적인 검증 로직이 들어갈 수 있습니다.
    }




    @DisplayName("[api] 게시글 -> 댓글 리스트 조회")
    @Test
    @Transactional
    void givenNothing_whenRequestingArticleCommentsFromArticle_thenReturnsArticleCommentsJsonResponse() throws Exception {
        // given
        // 게시글을 생성합니다.
        MvcResult articleResult = mvc.perform(post("/api/articles")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"제목\",\"content\":\"내용\",\"hashtag\":\"해시태그\"}"))
                .andExpect(status().isCreated())
                .andReturn();

        String articleLocation = articleResult.getResponse().getHeader("Location");

        // 게시글에 대한 댓글을 생성합니다.
        mvc.perform(post("/api/articleComments")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"article\":\"" + articleLocation + "\",\"content\":\"댓글내용\"}"))
                .andExpect(status().isCreated());

        // when
        // 해당 게시글의 댓글 목록을 조회합니다.
        String articleId = articleLocation.substring(articleLocation.lastIndexOf('/') + 1);
        mvc.perform(get("/api/articles/" + articleId + "/articleComments"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));
        // then: 결과를 검증하는 부분입니다 (위에서 이미 검증하였기 때문에 별도로 작성하지 않음).
    }


    @DisplayName("[api] 댓글 리스트 조회")
    @Test
    void givenNothing_whenRequestingArticleComments_thenReturnsArticleCommentsJsonResponse() throws Exception {
        // given
        //예시데이터 삽입



        // when
        mvc.perform(get("/api/articleComments"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));
        // then

    }


    @DisplayName("[api] 댓글 단건 조회")
    @Test
    @Transactional
    void givenNothing_whenRequestingArticleComment_thenReturnsArticleCommentJsonResponse() throws Exception {
        // given
        //예시데이터 삽입
        mvc.perform(post("/api/articles")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"title\":\"제목\",\"content\":\"내용\",\"hashtag\":\"해시태그\"}"))
                .andExpect(status().isCreated());
        mvc.perform(post("/api/articleComments")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"article\":\"http://localhost/api/articles/1\",\"content\":\"댓글내용\"}"))
                .andExpect(status().isCreated());

        // when
        mvc.perform(get("/api/articleComments/1"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));
        // then
    }
}

발생했던 문제
단위 테스트는 통과
근데 통합테스트는 에러

왜?

에러를 보면

        mvc.perform(get("/api/articleComments/1"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));

이런식으로 id값이 1이 맞는지 확인하는 방식이었는데
내 생각에는 개별 테스트 메서드 위에 @Transactional을 추가하면 그 메서드만 따로 트랜잭션이 되어서 다시 롤백 될 줄 알았는데 아니었다.
그래서 1이 아니라 다른 테스트에서 insert한 것이 남아서 2,3이 생성되었던 것이었다.

해결방법

...

@DisplayName("[api] 게시글 단건 조회")
@Test
@Transactional
void givenNothing_whenRequestingArticle_thenReturnsArticleJsonResponse() throws Exception {
    // given
    MvcResult result = mvc.perform(post("/api/articles")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\":\"제목\",\"content\":\"내용\",\"hashtag\":\"해시태그\"}"))
            .andExpect(status().isCreated())
            .andReturn();
    
    String location = result.getResponse().getHeader("Location");
    String articleId = location.substring(location.lastIndexOf('/') + 1);

    // when
    mvc.perform(get("/api/articles/" + articleId))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));
    // then
}

@DisplayName("[api] 게시글 -> 댓글 리스트 조회")
@Test
@Transactional
void givenNothing_whenRequestingArticleCommentsFromArticle_thenReturnsArticleCommentsJsonResponse() throws Exception {
    // given
    MvcResult articleResult = mvc.perform(post("/api/articles")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"title\":\"제목\",\"content\":\"내용\",\"hashtag\":\"해시태그\"}"))
            .andExpect(status().isCreated())
            .andReturn();
    
    String articleLocation = articleResult.getResponse().getHeader("Location");

    mvc.perform(post("/api/articleComments")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"article\":\"" + articleLocation + "\",\"content\":\"댓글내용\"}"))
            .andExpect(status().isCreated());

    // when
    String articleId = articleLocation.substring(articleLocation.lastIndexOf('/') + 1);
    mvc.perform(get("/api/articles/" + articleId + "/articleComments"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));
    // then
}

...

MvcResult를 사용하여 POST 요청의 결과를 캡처합니다.
MvcResult에서 Location 헤더를 가져와서 실제로 생성된 리소스의 URL을 얻습니다.
해당 URL에서 ID를 추출하여 후속 요청에 사용합니다.

profile
개발취준생

0개의 댓글