지난번에 우테코 프리코스 지원 과정에서 TDD를 시도해본 적이 있다.
이후 객체지향의 사실과 오해
라는 책도 읽고 객체지향의 본질을 0.01퍼센트 알게 되었는데... TDD를 하는 것이 강제로 객체지향스럽게 코드를 작성하도록 도와준다고 느꼈다.
그래서 이번 프로젝트를 TDD개발방법론을 적용하여 진행해보려고 한다.
@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;
}
BLOB, CLOB 데이터타입과 매핑 시켜준다.
일기 본문은 대용량 문자열이므로 @Lob
어노테이션을 사용한다.
날짜 시간 데이터타입을 매핑시킨다.
@SpringBootTest
: 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)@Transactional
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는 Data Transfer Object의 약자로 서비스 간 데이터 전달 시 사용되는 객체이다.
Page
객체를 service계층에서 controller에 전달한다고 가정할 때 이 객체를 그대로 사용하면 다음과 같은 문제가 발생한다.
즉, 더 유연하고 변화에 대응할 수 있도록 코드를 구성하기 위해선 Entity 와 DTO를 분리해서 사용해야한다.
@Getter
@RequiredArgsConstructor
public class PageRequest {
private final String title;
private final String content;
}
단위테스트의 가독성을 높이기 위해서는
1. 1개의 테스트 함수에 대해 assert
를 최소화하라
2. 1개의 테스트 함수는 1가지 개념만을 테스트하라
스프링 프레임워크에 대한 의존도를 낮춘다.
순환참조 문제를 방지할 수 있다.
객체의 불변성을 확보한다.
테스트 코드의 작성
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를 반환하도록 한다.
그동안 Spring을 사용해보면서 테스트코드를 작성할 때, 컨트롤러 계층은 도대체 어떻게 테스트하는 건지 의문이었다.
사실 test 코드 작성이 너무 귀찮았고, 중요하다고 아무리 말해도 와닿지 않아서 Mock조차 존재만 알고 사용해보지 않았었다.
MockMvc
라이브러리를 통해 프로그램의 엔드포인트에 Http요청을 임의로 보내고 결과를 확인할 수 있다.
Gson
은 google에서 만든 라이브러리로, Json에 대한 변환 기능을 제공한다.
@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
를 반환받는데, 해당 객체를 통해 결과값을 확인하고 검증할 수 있다.
@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();
}
resultAction
의 andReturn().getResponse()
함수를 호출하여 응답 데이터를 얻을 수 있다.
gson
라이브러리를 이용하여 응답값을 객체로 변환하고 검증한다.
생성자로 객체 정보를 설정하는 경우, 특정 필드에 대한 정보만 설정하기 위해서는 그 형식에 맞는 생성자가 각각 필요하다.
setter는 변경 가능성이 열려있다. 변경 가능성을 최소화하면서 필드 추가와 같은 코드의 변경에도 유연하게 대처할 수 있는 build패턴을 사용하는 것이 좋아보인다.
Mock이 제대로 작동하지 않아서 테스트가 통과되지 않았는데, 왜 그런지 삽질을 3시간은 한 것 같다.
repository test에만 @ExtendWith(MockitoExtension.class)
어노테이션을 클래스에 추가했었는데, 이걸 지우니까 제대로 작동했다.
왜인진 잘 모르겠지만, 버전이 바뀌면서 해당 어노테이션으로는 작동하지 않는 게 아닐까.. 시간이 좀 지난 참고자료를 볼 때 아직 사용하는 건지 공식 문서로 확인할 필요가 있을것 같다.