[PROJECT] 일기 저장 기능 및 테스트 구현

bin1225·2024년 11월 7일
0

Project_Only

목록 보기
4/9
post-thumbnail

지난번에 우테코 프리코스 지원 과정에서 TDD를 시도해본 적이 있다.
이후 객체지향의 사실과 오해라는 책도 읽고 객체지향의 본질을 0.01퍼센트 알게 되었는데... TDD를 하는 것이 강제로 객체지향스럽게 코드를 작성하도록 도와준다고 느꼈다.

그래서 이번 프로젝트를 TDD개발방법론을 적용하여 진행해보려고 한다.

Domain

Page

@Entity
public class Page {

    @Id @GeneratedValue
    private Long id;

    @Column(length = 50)
    private String title;
    @Lob
    private String content;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createDateTime;
    @Temporal(TemporalType.TIMESTAMP)
    private Date updateDateTime;
}

@Lob

BLOB, CLOB 데이터타입과 매핑 시켜준다.

  • BLOB: 대용량 바이너리 객체 (ex: 이미지,, 오디오, 동영상 등)
  • CLOB: 대용량 문자형 객체

일기 본문은 대용량 문자열이므로 @Lob어노테이션을 사용한다.

@Temporal

날짜 시간 데이터타입을 매핑시킨다.

  • @SpringBootTest : 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)
  • @Transactional

Repository

JPA를 사용하기 위해선 EntityManager가 필요하다.
EntityManager는 @Entity어노테이션이 붙은 객체를 관리한다.

  • 영속성 관리
  • 의존관계 주입
@Repository
public class PageRepository {

    @PersistenceContext
    private EntityManager em;

    public Page save(Page page) {
        em.persist(page);
        return page;
    }

    public Page find(Long id) {
        return em.find(Page.class, id);
    }
}

@PersistenceContext 어노테이션으로 엔티티 매니저를 주입할 수 있다.

DTO를 사용하는 이유

DTO는 Data Transfer Object의 약자로 서비스 간 데이터 전달 시 사용되는 객체이다.
Page객체를 service계층에서 controller에 전달한다고 가정할 때 이 객체를 그대로 사용하면 다음과 같은 문제가 발생한다.

  • 불필요한 정보 전달
  • 엔티티가 변경되면 API스펙을 변경해야한다.
  • 객체에 검증이 필요해지는 경우 기존 엔티티가 무겁고 복잡해진다.

즉, 더 유연하고 변화에 대응할 수 있도록 코드를 구성하기 위해선 Entity 와 DTO를 분리해서 사용해야한다.

PageRequest(Dto)

@Getter
@RequiredArgsConstructor
public class PageRequest {
    private final String title;
    private final String content;
}

좋은 단위테스트의 특징

단위테스트의 가독성을 높이기 위해서는
1. 1개의 테스트 함수에 대해 assert를 최소화하라
2. 1개의 테스트 함수는 1가지 개념만을 테스트하라

생성자 주입을 사용해야 하는 이유

  1. 스프링 프레임워크에 대한 의존도를 낮춘다.

  2. 순환참조 문제를 방지할 수 있다.

  3. 객체의 불변성을 확보한다.

    • setter나 일반 메소드로 주입 시 불필요한 변경 가능성이 존재한다.
  4. 테스트 코드의 작성

    • 필드주입 방법은 spring없이는 test코드 작성이 어려워지는데, 테스트에 spring 프레임워크를 사용하는 것은 테스트 비용을 증가시킨다.

Service

Mock

Mock라이브러리를 사용해보았다.
프로그램에서 어쩔 수 없이 객체간 의존 관계가 발생한다.
service는 repository에게 의존한다. 즉, repository의 기능을 이용하여 작동한다.

하지만 단위 테스트에서 권장되는 것은 그 자체의 기능만 검증하는 것이다. service계층의 메소드를 테스트할 때 repository의 기능까지 함께 테스트하면 단위테스트의 목적성이 불분명해진다.

이를 해결하기 위해 repository 객체를 가짜로 생성하고, 특정 작업 시 반환할 값을 미리 지정해둠으로써 repository는 배제하고 service에 대한 기능을 테스트할 수 있다.

초기 세팅

가짜객체로 생성할 필드에는 @Mock어노테이션을,
가짜객체를 주입받을 필드에는 InjectMocks어노테이션을 추가한다.

@SpringBootTest
public class PageServiceTest {

    @InjectMocks
    private PageService pageService;
    @Mock
    private PageRepository pageRepository;

	...
}

여러가지 기능이 있지만 doReturn을 사용해보았다.

    @Test
    @DisplayName("페이지 저장 테스트")
    public void savePage(){
        //given
        doReturn(page()).when(pageRepository).save(any(Page.class));

        //when
        PageResponse result = pageService.savePage(new PageRequest(title, content));

        //then
        Assertions.assertThat(result.getId()).isNotNull();
        Assertions.assertThat(result.getTitle()).isEqualTo(title);
        Assertions.assertThat(result.getContent()).isEqualTo(content);
    }

가짜 repository에 save함수를 호출하며 아무 Page클래스 객체를 넘겨주면 특정 page를 반환하도록 한다.

Controller

MockMvc

그동안 Spring을 사용해보면서 테스트코드를 작성할 때, 컨트롤러 계층은 도대체 어떻게 테스트하는 건지 의문이었다.
사실 test 코드 작성이 너무 귀찮았고, 중요하다고 아무리 말해도 와닿지 않아서 Mock조차 존재만 알고 사용해보지 않았었다.

MockMvc라이브러리를 통해 프로그램의 엔드포인트에 Http요청을 임의로 보내고 결과를 확인할 수 있다.

Gson은 google에서 만든 라이브러리로, Json에 대한 변환 기능을 제공한다.

PageControllerTest

@SpringBootTest
public class PageControllerTest {

    @InjectMocks
    private PageController pageController;
    @Mock
    private PageService pageService;

    private Gson gson;
    private MockMvc mockMvc;
    @BeforeEach
    void setUp() {
        gson = new Gson();
        mockMvc = MockMvcBuilders.standaloneSetup(pageController).build();
    }

mockMvc는 단일,spring 이용, web이용 3종류가 있는데 standaloneSetup을 사용했다.

perform()을 호출하여 요청을 생성하고 contentType과 content를 지정해준다.

생성한 요청에 대한 결과를 resultActions를 반환받는데, 해당 객체를 통해 결과값을 확인하고 검증할 수 있다.

MockMvc를 이용한 응답값 검증

@Test
    public void addPage() throws Exception {
        //given
        doReturn(pageResponse()).when(pageService).savePage(any(PageRequest.class));
        
        //when
        final ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/only/page/add")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(gson.toJson(pageRequest()))
        );
        //then
        resultActions.andExpect(status().isCreated());

        Assertions.assertThat(resultActions.andReturn().getResponse()).isNotNull();
        final PageResponse response = gson.fromJson(resultActions.andReturn()
                .getResponse()
                .getContentAsString(StandardCharsets.UTF_8),PageResponse.class);

        Assertions.assertThat(response.getId()).isNotNull();
    }

resultActionandReturn().getResponse()함수를 호출하여 응답 데이터를 얻을 수 있다.
gson라이브러리를 이용하여 응답값을 객체로 변환하고 검증한다.

그 외 배운점

builder 패턴

  • 필요한 데이터만 설정할 수 있음
  • 유연성을 확보할 수 있음
  • 가독성을 높일 수 있음
  • 변경 가능성을 최소화할 수 있음

생성자로 객체 정보를 설정하는 경우, 특정 필드에 대한 정보만 설정하기 위해서는 그 형식에 맞는 생성자가 각각 필요하다.

setter는 변경 가능성이 열려있다. 변경 가능성을 최소화하면서 필드 추가와 같은 코드의 변경에도 유연하게 대처할 수 있는 build패턴을 사용하는 것이 좋아보인다.

@ExtendWith(MockitoExtension.class)

Mock이 제대로 작동하지 않아서 테스트가 통과되지 않았는데, 왜 그런지 삽질을 3시간은 한 것 같다.

repository test에만 @ExtendWith(MockitoExtension.class) 어노테이션을 클래스에 추가했었는데, 이걸 지우니까 제대로 작동했다.

왜인진 잘 모르겠지만, 버전이 바뀌면서 해당 어노테이션으로는 작동하지 않는 게 아닐까.. 시간이 좀 지난 참고자료를 볼 때 아직 사용하는 건지 공식 문서로 확인할 필요가 있을것 같다.

Reference

0개의 댓글