개인적으로는 TDD를 좋아하지 않는 입장이다. 이유는 다음과 같다.
- 개발 하는것과 거의 같은 수준과 시간 노력을 들여 테스트코드를 짜야 한다 (즉, 일 2번 해야 한다)
- TDD를 통과했다고 하여 문제가 없다고 할 수 없다.
- TDD를 맹신하여 TDD 통과 후 테스트를 제대로 진행 하지 않고 배포할 가능성이 있다.
- 위의 이유로, 사람이 어짜피 테스트를 진행해야 하고, 이경우 TDD에서 발견하지 못한 문제점을 찾을 가능성이 높다.
- TDD 를 위해 테스트 코드를 작성하는데, 해당 코드 자체를 잘못 구현할 가능성이 있다. 이로인해 잘못된 테스트 통과가 될 수 있다.
- 이를 검증하기위해선 또다른 시간 및 노력이 들어간다.
그러나, 부분 테스트 용도로, 개발하는 도중 Repository만 테스트 해보고 싶을때 등 테스크코드 작성은 이용하면 잇점이 있다.
또는 특정 단위테스트를 수백번, 수만번 해야 할 경우, 사람으로 하는것은 어렵다.
또는 비즈니스 로직상으로는 문제가 없으나, 처리 로직에 문제가 있는경우 확인하는 용도로 좋다.
(오류를 잡아먹고 정상 동작하거나 최적화가 안되어 있어 성공 하는 경우)
기존 코드를 리팩토링 할때, 다른 부분에 영향을 받는지 여부를 확인하기 위해도 좋을거 같다. 그러나 스프링부트는 정말 분리가 잘 되어 있어서 그런일은 잘 없을거라고 본다.
리파지토리, 서비스 구현을 요약하자면 다음과 같다.
- TestClass 를 구현한다.
- Fixture 클래스를 구현한다.
- @Autowired 를 통해 필요한 객체들을 인젝션 해준다. (명시적으로 적어주어야 한다)
- @BeforeEach 어노테이션이 붙은 메서드를 만들어, 테스트 데이터를 생성해 준다.
- 각 테스트 메서드 실행전에 수행된다.
- 만일 @BeforeAll 을 할 경우, static 메소드로 생성해야 한다.
- 해당 클래스에서 테스트 실행시 딱 한번만 수행된다.
- 테스트 메소드를 작성하고, @Test 어노테이션을 붙여 테스트 메소드임을 알린다.
- 한가지 팁. assert, mockito 메소드들은 자동완성이 되지 않으므로, 아래를 import 하면, 자동완성을 사용할 수 있다.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
//컨트롤러 테스트시
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
테스트 데이터 생성을 위한 Fixture 개념
Mapper 클래스와 비슷한 느낌과 개념으로 이해하면 된다.
데이터 생성을 담당하는 클래스로, static 메소드를 통해 생성 객체를 제공 받아 테스트 데이터로 사용한다.
public class PostFixture {
public static Post createPost(String title, String content) {
return Post.builder()
.title(title)
.content(content)
.author("test")
.password("0000")
.tags(List.of("tag1", "tag2"))
.build();
}
}
1. Repository 테스트
Test 클래스 생성시 위치는, 테스트 대상이 되는 패키지명과 일치하는게 좋다.
(예 : com.simple.test.repository 에 repository가 있다면, test 클래스 또한 test -> com.simple.test -> repository 패키지 생성하여 여기에 추가 한다.
테스트 대상은 주로 JPA 중 쿼리를 수동작성했거나, 복잡한 조건이 들어가 있는 항목들이 우선대상이 된다.
@DataJpaTest 를 클래스에 붙여줌으로서 JPA 전용 테스트를 할 수 있다.
@DataJpaTest
public class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@BeforeEach
public void setup() {
postRepository.deleteAll();
// 테스트 데이터 준비
postRepository.save(PostFixture.createPost("t1","c1"));
postRepository.save(PostFixture.createPost("t2","c2"));
postRepository.save(PostFixture.createPost("t3","c3"));
postRepository.save(PostFixture.createPost("t4","c4"));
}
@Test
@DisplayName("save")
void savePost() {
Post post = Post.builder().title("t2")
.content("c2")
.author("a2")
.password("p2")
.tags(List.of("tag1", "tag2"))
.build();
Post saved = postRepository.save(post);
assertNotNull(saved);
assertEquals(post.getTitle(), "t2");
}
....
2. Mockito 를 이용한 서비스 테스트
테스트 우선순위가 높은 항목들 : 비즈니스 로직이 복잡한 경우, 트랜잭션이 필요한 메소드. 단순히 repository를 호출하는 메소드 또는 단순히 DTO 변환만 하는 메소드의 경우도 테스트 중요도가 떨어진다.. 서비스 테스트는 단위테스트, 통합테스트로 나눠진다.
단위테스트의 경우,
@ExtendWith(MockitoExtension.class) 어노테이션을 테스트 클래스에 붙인다.
Repository를 실제 연결해서 테스트 하지 않고, Mock 객체를 통해 Repository를 흉내내어 서비스 모듈을 테스트 한다.
- spy 객체 : 일부는 실제 데이터를 가지고 오고, 일부는 가짜 데이터를 가져와서 처리한다. 예를들어 외부 API를 이용할 경우, 해당 API만 가짜 데이터를 이용하는 경우
@Mock
Mock 객체를 만드는데, 보통 Repository 같이 Service에서 실제 인젝션 받는 객체에 붙여준다. 또는 외부 라이브러리나 API를 이용할때 해당 객체를 만들기 위해서도 사용한다. (빈 으로 동작하는게 아닌 순수 객체로 동작한다)
@InjectMocks
테스트할 서비스 객체에 붙여준다.
when, thenReturn
Repository 등 Mock 객체의 메소드가 실제로 동작하지 않기 때문에, when 조건을 통해 어떤 메소드가 동작할때, 결과값으로 thenReturn 이 나와야 한다는것을 명시한다. 이를 통해 실제 Mock 객체가 정상 동작하여 결과값을 내놓은것을 가정한다.
@ExtendWith(MockitoExtension.class)
public class PostServiceTest {
@InjectMocks
private PostService postService;
@Mock
private PostRepository postRepository;
@Test
@DisplayName("저장 테스트")
void savePost()
{
PostCreateRequest request = PostCreateRequest.builder().title("t3").content("c3").build();
Post post = Post.builder().title("t3").content("c3").build();
when(postRepository.save(any(Post.class))).thenReturn(post);
PostResponse response = postService.savePost(request);
assertEquals(response.title(), "t3");
verify(postRepository, times(1)).save(any(Post.class));
}
....
}
통합테스트의 경우,
@SpringbootTest 어노테이션을 테스트 클래스에 붙여준다. 보통은 @Transactional 도 같이 붙인다. (엔티티 등 모든항목 불러오므로 부하가 있다)
(서비스에 트랜잭션이 붙어있는 경우가 대부분이므로) Repository 테스트 또한 같이 하므로 Mock을 이용치 않는다.
@SpringBootTest
@Transactional
public class PostServiceTotalTest {
@Autowired
private PostService postService;
@Autowired
private PostRepository postRepository;
private Post postMock;
@BeforeEach
void savePost() {
postRepository.deleteAll();
postMock = postRepository.save(Post.builder()
.author("codeit")
.password("123456")
.title("Original Title")
.content("Original Content")
.build());
}
@Test
public void updatePost() {
PostUpdateRequest request = PostUpdateRequest.builder().title("title").content("NewContent").password("123456").build();
PostResponse response = postService.updatePost(postMock.getId(), request);
Post post = postRepository.findById(postMock.getId()).get();
assertEquals(response.content(),request.content());
assertEquals(post.getContent(),response.content());
}
.....
3. Controller 테스트
컨트롤러 테스트는, Service, Repository 테스트와는 조금 방식이 다르다.
일단 컨트롤러 중 테스트 대상은 @Valid 를 통한 값 검증, 커스텀 예외처리가 있는 엔드포인트 위주로 테스트 진행하는것이 좋다. 간단히 Service를 호출하여 리턴하는 방식은 굳이 테스트 하지 않아도 된다.
@WebMvcTest 를 클래스의 어노테이션을 붙여 테스트 한다.
@MockitoBean (텅빈 객체를 만드는 Mock을 Bean 으로 까지 등록해준다)
MockMvc 자료형의 객체를 @Autowired 로 주입받아 사용한다.
ObjectMapper 또한 @Autowired 로 주입받아 사용한다. (json 객체 상호 변환을 위함)
@WebMvcTest(PostController.class)
public class PostControllerTest
{
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockitoBean
private PostService postService;
//Audit 등 빈을 찾지 못할경우 오류 해결
@MockitoBean
private JpaMetamodelMappingContext jpaMappingContext;
@Test
@DisplayName("create test")
void createPost throws Exception
{
PostCreateRequest request = PostCreateRequest.builder()
.author("aa")
.password("0000")
.title("title")
.content("content")
.build();
PostResponse response = PostResponse.builder()
.id(1L)
.author("aa")
.title("title")
.content("content")
.build();
when(postService.savePost(request).thenReturn(response);
mockMvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.andExpect(status().isCreated()));
}
....
}
** Repository를 포함하여 DB 테스트 하는 경우, (단위든 통합이든) Entity Manager 를 인젝션 받아 flush, clear 함으로서 실제 DB 동작을 수행할 수 있다. (예 : 실제 DB 에 업데이트된 내용이 반영되었는지) 그렇지 않으면 Persist Context를 통해 테스트는 통과하나, 실제 DB에 반영되었는지는 확인 할 수 없다. 하지만 ORM을 믿는것도.....
'Backend > SpringBoot' 카테고리의 다른 글
| QueryDSL (0) | 2024.10.30 |
|---|---|
| DB 연결 설정 (0) | 2024.10.29 |
| Query 어노테이션(JPQL) 및 JPA의 Specification (0) | 2024.10.25 |
| Bean 어노테이션 (W.Configuration, Component 어노테이션) (1) | 2024.10.25 |
| Spring Security 기본 설정 (0) | 2024.10.25 |