오봉이와 함께하는 개발 블로그

스프링 week_4 본문

자바/스프링 튕튕

스프링 week_4

오봉봉이 2021. 11. 2. 17:42
728x90

Artineer Spring 4주차 강의노트

테스트

테스트는 개발자들에게 매우 중요하다.

테스트 패키지는 소스(원본) 패키지와 동일한 구조, 테스트하는 클래스 파일은 대상이 되는 클래스파일 뒤에 test를 붙여 만든다.

각 layer에 대한 특성이 다르기 때문에 테스트 코드를 작성하는 방법이 다 다르다.

@Test : 테스트 코드로 동작시키는 어노테이션

  • 테스트 코드에 들어가는 함수 명은 어떤 것을 테스트 하려는지 명시를 해야 한다.(한글로 작성 가능)
  • 테스트 코드는 무언가 주어지고(given), 실행 했을 때(when) 어떤 결과가 나와야 한다.(then) 라는 패턴으로 작성
  • 유닛테스트
    • 기능, 함수가 정상적으로 동작 하는지 검증하기 위한 코드
    • 테스트 자체의 목적도 있지만 함수의 동작을 명시하는 문서의 역할도 하기 때문에 자세하게 적는 것이 중요하다.

도메인 유닛 테스트

Article.update() 테스트 코드

ArticleTest.java

@Test
public void update_호출하면_title_content_필드_값이_변경되어야_한다() {
    // given -> 테스트 코드를 작성하기 위해 필요한 선처리 작업
    Article article = Article.builder()
            .title("title before")
            .content("content before")
            .build();

    // when -> 업데이트 함수를 통해 변경
    article.update("title after", "content after");

    /// then -> 이런 결과가 나와야 한다.
    assertThat("title after").isEqualTo(article.getTitle());
    assertThat("content after").isEqualTo(article.getContent());
}

TDD(Test Driven Development)

무조건 실패하는 형태로 코드를 작성하고(실제 내용물이 없기 때문에), 테스트 코드가 성공하도록 하는 코드를 작성한다.

  • 테스트 코드 먼저 작성하고 함수의 스펙을 정하고 실제로 구현

    Article.of() 테스트 코드

    ArticleTest.java

    class ArticleTest {
      // ...
    
      @Test
      public void of_호출하면_Article_객체를_반환해야_한다() {
          // given
          ArticleDto.ReqPost request = ArticleDto.ReqPost.builder()
                  .title("title")
                  .content("content")
                  .build();
    
          // when
          Article article = Article.of(request);
    
          // then
          assertThat(article.getTitle()).isEqualTo("title");
          assertThat(article.getContent()).isEqualTo("content");
      }
    }

Article.java

public class Article {
  // ...
  public static Article of(ArticleDto.ReqPost from) {
    return Article.builder()
//                .title(from.getTitle())
//                .content(from.getContent())
            .build();
  }

  // ...
}
  • of()함수가 테스트 코드를 통과할 수 있도록 변경 하려면 .title(from.getTitle())과 .content(from.getContent())의 주석을 제거

서비스 계층 유닛 테스트

  • 서비스 계층은 특성상 기본적으로 의존성을 가지고 있다.
  • 의존성 때문에 조금은 어렵다고 느낄 수 있다.
  • @ExtendWith : 타겟 클래스에 의존성이 존재하기 때문에 가짜 객체를 만들어주는 어노테이션
    • 가짜 객체는 스펙으로 가지고 있는 특징들만 가지고 온다
    • 즉, 구현체는 담겨있지 않다. (함수명, 파라미터, 반환타입 등)
  • @InjectMocks : 가짜 객체 대상을 선언하는 어노테이션 (클래스)
  • @Mock : 가짜 객체를 만드는 어노테이션 (객체)
  • Service도 Repository도 가짜 객체를 사용

findById 테스트

ArticleServiceTest.java

@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {
  @InjectMocks
  private ArticleService articleService;

  @Mock
  private ArticleRepository articleRepository;

  @Test
  public void findById_id_에_맞는_Article_객체를_가져온다() {
    // given
    Long idStub = 1L; // Stub은 가짜 값을 표현한다.
    Optional<Article> articleOptStub = Optional.of(Article.builder()
            .id(idStub).title("title").content("content").build());
    when(articleRepository.findById(idStub)).thenReturn(articleOptStub); 
    // when -> 가짜 객체가 해야하는 역할(적지 않으면 가짜 객체는 실체가 없기 때문에 null이 반환됨

    // when
    Article result = articleService.findById(idStub);

    // then
    assertThat(result.getId()).isEqualTo(articleOptStub.get().getId());
  }
}

예외처리 테스트

ArticleServiceTest.java

@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {
    // ...
  @Test
  public void findById_id_에_맞는_Article_객체가_없다면_DATA_IS_NOT_FOUND_오류발생() {
    // given
    Long idStub = 1L;

    // when
    when(articleRepository.findById(idStub)).thenReturn(Optional.empty());

    // then
    assertThatThrownBy(() -> articleService.findById(idStub))
            .isInstanceOf(ApiException.class)
            .hasMessage("article is not found");
  }
}

테스트 커버리지

  • 테스트 코드를 어느 수준으로 작성 하는지(모든 케이스, 필요한 부분만, 짜지 않는다. 등) 회사마다 다르다.
  • 테스트 코드를 많이 작성하면 튼튼한 시스템을 구축할 수 있다.
  • 참고할 수 있는 문서가 되기 때문에 리팩토링에 있어 부담을 덜어준다.
  • 하지만, 일이 더 늘어나기 때문에 시간적으로 부담이 될 수 있다.

AnyMatcher

  • 정해진 특정 값을 넣어주지 않고 같은 타입이 들어오게 된다면 정상적으로 동작되는 테스트 코드를 작성할 수 있게 도와준다.

  • 하지만 사용하려면 모든 값을 any로 사용해야 한다.

    AnyMatcher를 사용한 테스트 코드

    ArticleServiceTest.java

    @ExtendWith(MockitoExtension.class)
    class ArticleServiceTest {
      @InjectMocks
      private ArticleService articleService;
    
      @Mock
      private ArticleRepository articleRepository;
    
      @Test
      public void findById_id_에_맞는_Article_객체를_가져온다() {
          // given
          Long idStub = anyLong();
          Optional<Article> articleOptStub = Optional.of(Article.builder()
                  .id(idStub).title("title").content("content").build());
    
          when(articleRepository.findById(idStub)).thenReturn(articleOptStub);
          // when -> 가짜 객체가 해야하는 역할(적지 않으면 가짜 객체는 실체가 없기 때문에 null이 반환됨
          // when
          Article result = articleService.findById(idStub);
    
          // then
          assertThat(result.getId()).isEqualTo(articleOptStub.get().getId());
      }
    
      @Test
      public void findById_id_에_맞는_Article_객체가_없다면_DATA_IS_NOT_FOUND_오류발생() {
          // given
          Long idStub = anyLong();
    
          // when
          when(articleRepository.findById(idStub)).thenReturn(Optional.empty());
    
          // then
          assertThatThrownBy(() -> articleService.findById(idStub))
                  .isInstanceOf(ApiException.class)
                  .hasMessage("article is not found");
      }
    }

Repository 유닛 테스트

  • 데이터베이스와 관련된 테스트 코드 작성은 어렵다.

  • 어떤 Insert가 성공하고 updata를 하는 등 수정이 이루어지는 테스트를 했을 때 실제 데이터베이스에 반영되고 롤백이 안된다면 곤란.

    • 롤백을 하는 코드도 모두 작성해야 하기 때문에 어려웠다.
  • @DataJpaTest : 위에 이슈들을 해결할 수 있게 해주는 어노테이션

    • H2 DB를 자동으로 벤더로 적용시켜줌
  • 의존성이 따로 없기 때문에 Mock을 사용하지 않고 실제 객체를 사용하기 위해 빈을 가지고 오는 SpringExtension.class를 사용

    • @ExtendWith(SpringExtension.class) : 실제 빈을 가지고 와줌
    • Repository에 해당하는 빈만 가지고 온다.
  • 테스트 코드에서는 순환참조 이슈를 생각하지 않고 필드주입으로 작성 (자유롭게 사용해도 된다.)

    테스트 코드 작성을 위해 간단한 쿼리메소드

    ArticleRepository.java

    public interface ArticleRepository extends CrudRepository<Article, Long> {
      List<Article> findByTitle(String title);
    }

    Repository 유닛 테스트 코드 작성 (findByTitle)

    ArticleRepositoryTest.java

    @ExtendWith(SpringExtension.class)
    @DataJpaTest
    class ArticleRepositoryTest {
      @Autowired
      private ArticleRepository articleRepository;
    
      @Test
      public void findByTitle_title_조회하면_일치하는_Article_객체들이_반환된다() {
          // given
          String javaStub = "Java";
          String pythonStub = "Python";
    
          articleRepository.save(Article.builder().title(javaStub).build());
          articleRepository.save(Article.builder().title(javaStub).build());
          articleRepository.save(Article.builder().title(pythonStub).build());
    
          // when
          List<Article> articles = articleRepository.findByTitle(javaStub);
    
          // then
          assertThat(articles.size()).isEqualTo(2);
          assertTrue(articles.stream().allMatch(a -> javaStub.equals(a.getTitle())), "title 필드 값이 모두 " + javaStub + " 인가?");
      }
    }

Controller 유닛 테스트

  • @WebMvcTest(ArticleController.class) : Controller와 관련된 빈만 가져오는 어노테이션

    ArticleRepositoryTest.java

    @WebMvcTest(ArticleController.class)
    class ArticleControllerTest {
    

}

- Controller는 Repository와 다르게 Service에 대한 의존성이 있다
    - Service에 대한 의존성이 있기 때문에 가짜 객체를 사용해야 하는데 Controller는 실제 객체를 사용하고 Service만 가짜 객체를 사용하고 싶다.
    - @MockBean을 사용하면 객체를 스프링 컨테이너에 등록
#### ArticleRepositoryTest.java
~~~ java
@WebMvcTest(ArticleController.class)
class ArticleControllerTest {
    @MockBean
    ArticleService articleService;
}
  • Controller 함수들은 API 진입점으로 활용되기 때문에 API를 호출할 수 있는 클라이언트 MockMvc 추가

    • WebMvcTest를 등록하면 MockMvc도 사용가능

      ArticleRepositoryTest.java

      @WebMvcTest(ArticleController.class)
      class ArticleControllerTest {
      @MockBean
      ArticleService articleService;
      @Autowired
      MockMvc mvc;
      }

      MockMvc를 활용해서 테스트 코드 구현

      ArticleRepositoryTest.java

      @WebMvcTest(ArticleController.class)
      class ArticleControllerTest {
      @MockBean
      ArticleService articleService;
      @Autowired
      MockMvc mvc;
      
      @Test
      @DisplayName("Response.ok() 함수 호출시 code 값이 ApiCode.SUCCESS 값을 가져야 한다.")
      // 한글로 만들기 싫을 때 사용할 수 있음.
      public void get_whenYouCallOk_ThenReturnSuccessCode() throws Exception {
        // given
        Long idStub = 1L;
        when(articleService.findById(idStub)).thenReturn(Article.builder().id(idStub).build());
      
        // when, then
        mvc.perform(get("/api/v1/article/" + idStub)
                        .characterEncoding("utf-8")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print()) // print를 사용하지 않는다면 내용 출력이 안됨.
                .andExpect(status().isOk());
      }
      }

      실제 응답을 받는 값을 얻고 싶다면…

      ArticleRepositoryTest.java

      @WebMvcTest(ArticleController.class)
      class ArticleControllerTest {
      @MockBean
      ArticleService articleService;
      @Autowired
      MockMvc mvc;
      
      @Test
      @DisplayName("Response.ok() 함수 호출시 code 값이 ApiCode.SUCCESS 값을 가져야 한다.")
      public void get_whenYouCallOk_ThenReturnSuccessCode() throws Exception {
        // given
        Long idStub = 1L;
        when(articleService.findById(idStub)).thenReturn(Article.builder().id(idStub).build());
      
        // when
        String response = mvc.perform(get("/api/v1/article/" + idStub)
                        .characterEncoding("utf-8")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
      
        // then
        assertThat(ApiCode.SUCCESS.getName()).isSubstringOf(response);
      }
      }

이후 내용은 28일

728x90

'자바 > 스프링 튕튕' 카테고리의 다른 글

스프링 week_6  (0) 2021.11.22
스프링 week_5  (0) 2021.11.15
스프링 week_3  (0) 2021.11.02
스프링 week_2  (0) 2021.11.02
스프링 week_1  (0) 2021.11.02
Comments