목표 : 테스트코드 작성법을 공부하고 정리한다.
카카오테크캠퍼스를 하다보니까 테스트코드의 중요성을 알게 되었다.
근데 나는 남이 짜준 테스트코드를 돌리는 것 이외에는 딱히 해본 적이 없어서 이 부분을 구현하는데 힘들었다.
그리고 테스트코드를 짜는 방법을 확실하게 몰라서 더 부담을 느끼는 것 같아서 이번에 쭉 정리를 해보려고 한다.
테스트코드를 작성하는 이유
- 디버깅 비용 절감
- 코드변경에 대한 불안감 해소
- 더 나은 자료
- 좋은 코드는 테스트하기 쉽다.
- 테스트 자동화
@SpringBootTest : 테스트에 필요한 핵심 기능 라이브러리
@Junit : java에서 독립된 단위 테스트를 지원해주는 프레임워크
@Test 어노테이션마다 독립적으로 테스트가 진행된다.
단위테스트 VS 통합테스트
단위테스트는 시간과 비용면에서 좋고, test코드 자체가 하나의 문서가 되지만, 가짜객체를 사용해야하고, 실제 운영환경과 다른 답변을 내놓을 수 있다.
통합테스트는 실제 객체를 사용하므로 가짜객체를 사용하지 않아도 된다. 하지만, 테스트 하나에 많은 비용이 들어가고, 문제가 생기면 어떤 계층에서 생긴건지 파악하기 어렵다.
단위테스트는 Given, When, Then으로 명확하게 작성해야한다.
- given : 테스트를 진행할 행위를 위한 사전 준비
- when : 테스트를 진행할 행위
- then : 테스트를 진행한 행위에 대한 검증
AssertJ 라이브러리
assertThat는 값 검증에 쓰인다.
assertThat(실제값).isEqualTo(기댓값)
assertThat(실제 객체).isInstanceOf(객체 예상 타입)
assertThat(실제값).isNull()
assertThatThrownBy는 예외 검증에 쓰인다.
예외가 발생하면 테스트가 통과하고, 발생하지 않으면 테스트가 실패한다.
도메인 테스트
@Test
@DisplayName("멤버가 생성되는지 확인하는 테스트")
void createMember(){
/*
given
*/
Member member = Member.builder().age(10).name("hi").build();
/*
when, then
*/
Assertions.assertThat(member.getAge()).isEqualTo(10);
Assertions.assertThat(member.getName()).isEqualTo("hi");
}
위 코드는 member이라는 도메인을 테스트하는 코드이다.
@Test어노테이션을 사용해야하고, @DisplayName을 통해서 테스트 시 나오는 테스트명을 정할 수 있다.
@Test
@DisplayName("멤버의 나이 바뀌는지 확인하는 테스트")
void changeAgeTest(){
/*
given
*/
Member member = Member.builder().age(10).name("hi").build();
/*
when
*/
member.changeAge(13);
/*
then
*/
assertThat(member.getAge()).isEqualTo(12);
}
이렇게 도메인 내에 특정 메서드가 있으면 이에 대한 테스트도 진행할 수 있다.
JPA테스트
@DataJpaTest : JPA를 사용하는 레파지토리를 테스트할 때 사용되는 어노테이션이다.
@DataJpaTest는 @Transaction을 포함하고 있어서 1개의 테스트가 끝나면 Rollback해 다른 테스트에게 영향을 미치지 않는다
@DataJpaTest
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
@DisplayName("멤버 만들기")
void createMember(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Member member2 = Member.builder().name("hi2").age(20).build();
/*
when
*/
Member result1 = memberRepository.save(member1);
Member result2 = memberRepository.save(member2);
/*
then
*/
assertThat(result1.getAge()).isEqualTo(member1.getAge());
assertThat(result2.getAge()).isEqualTo(member2.getAge());
}
@Test
@DisplayName("멤버의 리스트를 반환 하는지 확인")
void MemberList(){
/*
given
*/
Member member1 = Member.builder().name("hi1").age(10).build();
Member member2 = Member.builder().name("hi2").age(20).build();
memberRepository.save(member1);
memberRepository.save(member2);
/*
when
*/
List<Member> result = memberRepository.findAll();
/*
then
*/
assertThat(result.size()).isEqualTo(2);
}
}
위 코드는 memberRepository에 값이 잘 들어가는지, 원하는 값들이 잘 return되는지를 테스트한다.
객체 주입은 필드로 주입하는 방식을 사용한다.
Service Test
Service 계층은 Repository객체를 Spring에게 주입받는다.
따라서 Repository는 가짜 객체로서 응답을 설정해야한다.
Junit5기능을 사용하고 가짜 객체를 사용하기 떄문에 @ExtendWith(SpringExtention.class)를 붙여줘야 한다.
@ExtendWith(SpringExtension.class)
public class MemberServiceTest {
// Test 주체
MemberService memberService;
// Test 협력자
@MockBean
MemberRepository memberRepository;
// Test를 실행하기 전마다 MemberService에 가짜 객체를 주입시켜준다.
@BeforeEach
void setUp(){
memberService = new MemberServiceImpl(memberRepository);
}
}
@BeforeEach는 Test를 실행하기 전 항상 실행하도록 하는 어노테이션이다. 여기서는 가짜객체를 주입하는 데 사용된다.
@MockBean은 가짜 객체를 만드는 역할은 한다. Test의 협력자이다.
MemberService : Test의 주체로서 가짜 객체를 주입받고, 자신의 로직을 실행하고 결과를 가지고 검증한다.
Mock이란
실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어서 제대로 구현하기 어려울 때 가짜 객체를 만들어 사용하는데 이게 mock이다.
@Test
@DisplayName("멤버 생성 성공")
void createMemberSuccess(){
/*
given
*/
Member member3 = Member.builder().name("hi3").age(10).build();
ReflectionTestUtils.setField(member3,"id",3l);
Mockito.when(memberRepository.save(member3)).thenReturn(member3); // 가짜 객체 응답 정의
/*
when
*/
Long hi3 = memberService.createMember("hi3", 10);
/*
then
*/
assertThat(hi3).isEqualTo(3L);
}
위 코드는 save 기능을 테스트한다.
우선 member3객체를 만드는데, ReflextionTestUtils에서 private로 된 필드의 값을 설정한다.
Repository에다가 member3객체를 넣으면 member3을 리턴하도록 시킨다.
그 다음 Service에 createMemeber 객체들을 넣고 반환값의 아이디가 3L인지 확인한다.
이게 가능한 이유는 member 도메인에다가 eqauls를 오버라이딩함으로서 같은 이름, 나이를 가진 객체는 동일시 했기 떄문에다.
ControllerTest
@WebMvcTest(MemberController.class)
public class MemberControllerTest {
@Autowired
MockMvc mvc;
@MockBean
MemberServiceImpl memberService;
}
Test의 주체는 MemberController이므로 이를 @WebMvcTest에 선언을 해야한다.
협력자인 MEmberService는 협력자니까 @MocBean을 등록해주고, Test에 응답을 정의한다
MockMvc는 실제로 서블릿 컨테이너를 사용하지 않고, 테스트용으로 Mvc기능을 사용할 수 있게 해주는 역할을 한다.
테스트 때 생성되는 WebApplicationContext에서 주입받는다.
@Test
@DisplayName("리스트 반환받기")
void getList() throws Exception {
/*
given
*/
List<MemberResponseDto.ListDto> list = List.of(new MemberResponseDto.ListDto("asd", 10)
, new MemberResponseDto.ListDto("fsd", 12));
Mockito.when(memberService.findAll()).thenReturn(list);
/*
when then
*/
mvc.perform(MockMvcRequestBuilders.get("/members").contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("fsd"))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("asd"));
}
serviceTest할 때와 같이 리스트를 만들고, mockito로 이를 응답하도록 설정한다.
그 다음 mvc.perform으로 테스트하는데, mvc.perform(MockMvcRequestBuilders.get().contentType()에서는 컨트롤러에게 요청을 보내는데, uri를 만들고, contentType를 지정한다.
andDo는 요청에 대한 처리를 한다. 여기서는 응답값을 콘솔에 출력한다.
andExpect : 검증하는 로직이다.
MockMvcResultMatcher.status()는 HTTP 상태 코드를 검증하고, jsonPath는 Json로 넘어온 것들에 대한 값을 검증한다.
jsonPath("$. name"). value("fsd")는 단일 객체에 대한 값 검증이고, jsonPath("$[1]. name"). value("asd): 리스트를 반환받았을 때 지정하여 검증이다.
통합테스트
통합테스트는 전체적인 Spring에 쓰이는 Bean들이 등록된다.
@SpringBootTest는 통합 테스트를 진행하기 위한 어노테이션이다.
이 때 주의해야 할 점은 @Transaction을 포함하지 않고 있기 때문에 Repository도 사용하고 있다면 @Transaction도 붙여서 RollBack를 실행해야한다.
public class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
}
@Test
@DisplayName("멤버 만들기")
void createMemberSuccess(){
Long memberId = memberService.createMember("hi1", 10);
assertThat(memberId).isEqualTo(1l);
}
@Test
@DisplayName("이름 중복으로 만들기 실패")
void createMemberFail(){
Long memberId = memberService.createMember("hi1", 10);
assertThatThrownBy(() -> memberService.createMember("hi1",12)).isInstanceOf(IllegalStateException.class);
}
이렇게 단위테스트보다 간단한 것을 볼 수 있다.
소감 : 공부해보니까 내가 생각한만큼 그렇게 어렵지 않아서 이제까지 만든 프로젝트에 한번 적용해봐야겠다는 생각이 들었다.
https://tech.inflab.com/20230404-test-code/
테스트 코드를 왜 그리고 어떻게 작성해야 할까?
테스트 코드가 필요한 이유와 잘 작성하는 방법에 대해 공유합니다.
tech.inflab.com
Springboot Test 코드 작성
Test 코드를 작성하는 법을 알아보기 전에 Test 코드의 필요성에 대해서 알아보겠습니다. 1. 왜 Test 코드를 작성하는가? 크게 2가지 이유가 있습니다.' 1-1. Test 코드를 작성하지 않고 결과를 검증하
dingdingmin-back-end-developer.tistory.com
@MockBean을 사용한 통합(Controller)테스트 - 배종진
@MockBean을 사용한 통합(Controller)테스트
velog.io
'활동정리 > 모각코' 카테고리의 다른 글
2024 하계 모각코 6회차 (0) | 2024.07.28 |
---|---|
2024 하계 모각코 5회차 (0) | 2024.07.23 |
2024 하계 모각코 3회차 (1) | 2024.07.16 |
2024 하계 모각코 2회차 (2) | 2024.07.13 |
2024 하계 모각코 1회차 (0) | 2024.07.13 |