[JUnit] 테스트 케이스 작성법
테스트 원칙 (F.I.R.S.T)
1. F (Fast) : 테스트는 빨라야 한다.
2. I (Independent) : 각 테스트는 독립적으로 실행되어야 한다.
3. R (Repetable) : 테스트는 반복해서 실행될수 있어야 한다.
4. S (Self-Validating) : 테스트 결과는 성공(true) or 실패(flse)로 나타나야 한다
5. T (Timely) : 테스트는 적절한 시기에 작성되어야 한다
테스트 작성법
- Positive뿐만 아니라
Negative
케이스도 작성하라 - 구현기반 테스트가 아닌 설계기반 테스트 케이스를 작성하라
- 구현은 언제든지 변경이 가능하기에 현재 구현방식에 의존하지 않은 케이스로 작성
- 경계값 분석 케이스를 작성하라
- 테스트 하나에 최소의 assert문을 사용하라
- 테스트 하나에 1개의 개념만 테스트해라
- 되도록 테스트를 위한 setup은 테스트 메서드 안에 위치시켜라
- 테스트가 길어져도 상관 없다
- 검증에 필요한 값만 설정해라
BDD기반 3-step
(1) given : 주어진 것
(2) when : 실행했을때
(3) then : 결과
// 도메인
@Getter
@Setter
@NoArgsConstructor
public class Member {
private Long id;
private String name;
}
// 서비스
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Long signup(MemberDTO dto) {
return memberRepository.save(dtoToEntity(dto)).getId();
}
public MemberDTO getOne(Long id) {
Member entity = memberRepository.findById(id)
.orElseThrow(() -> new IllegalStateException("해당 회원이 존재하지 않습니다."));
return entityToDTO(entity);
}
default dtoToEntity(MemberDTO dto) {
return Member.builder()
.id(dto.getId())
.name(dto.getName())
.build();
}
default entityToDTO(Member entity) {
return MemberDTO.builder()
.id(entity.getId())
.name(entity.getName())
.build();
}
}
// 리포지토리 인터페이스
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
}
// 리포지토리 구현체
public class MemberRepositoryImpl implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public void clearStore() {
store.clear();
}
}
1. Repository 계층
@SpringBootTest
class MemberRepositoryTest {
@Autowired
private MemberRepository repository;
@AfterEach
public void afterEach() { // 여러번 테스트를 위해 매 테스트가 끝나면 cleanup
repository.clearStore();
}
@Test
public void save() {
// 1. given
Member member = new Member();
member.setName("spring");
// 2. when
repository.save(member);
// 3. then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
}
◼︎ JPA 테스트
-
@DataJpaTest
사용- JPA를 다루는데 필요한 Bean만 가져오므로
@SpringBootTest
보다 가볍다 - 해당 어노테이션은
@ExtendWith(SpringExtend.class)
을 포함한다 - 해당 어노테이션은
@Transactional
을 포함하기 때문에 테스트 후 자동으로 RollBack 된다
- JPA를 다루는데 필요한 Bean만 가져오므로
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // 자동설정되는 테스트 내장 DB 해제
@Import(TestConfig.class) // Test용 config 파일 import
class MemberRepositoryTest {
@Autowired
private MemberRepository repository;
@Test
public void save() {
// 1. given
Member member = new Member();
member.setName("spring");
// 2. when
repository.save(member);
// 3. then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
}
@TestConfiguration
public class TestConfig { // QueryDSL 설정 추가
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
→ 슬라이스 테스트시 해당 Bean이 Persistance Layer
에 속하지 않아 Bean을 찾을 수 없다
→ Test Configuration에 따로 빈을 등록해 import 해준다
2. Service 계층
◼︎ SpringBootTest 사용할 경우
@SpringBootTest(classes = MemberServiceImpl.class)
class MemberServiceTest {
@Autowired
MemberService memberService;
@MockBean
MemberRepository memberRepository;
@Test
public void signup() {
...
}
◼︎ Mockito 프레임워크
-
@ExtendWith(MockitoExtension.class)
: Mockito 프레임워크 사용 -
@Mock
: 해당 객체의 Mock(가짜 객체) 생성 -
@Spy
: 해당 객체의 실제 객체 사용 -
@InjectMocks
:@Mock
,@Spy
으로 선언된 의존 객체를 해당 객체에 주입
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@InjectMocks
private MemberServiceImpl memberService;
@Mock
private MemberRepository memberRepository;
@Test
public void signup() {
// 1. given
MemberDTO dto = MemberDTO.builder()
.id(1L)
.name("europani")
.build();
Member entity = memberService.dtoToEntity(dto);
// 리턴값 설정
given(memberRepository.save(any(Member.class)))
.willReturn(entity);
// 2. when
Long id = memberService.signup(dto);
// 3. then
assertThat(id).isEqualTo(1L);
verify(memberRepository).save(any(Member.class));
}
@Test
@DisplayName("회원 조회 성공")
void getOne() {
// given
Long id = 1L;
MemberDTO dto = MemberDTO.builder().id(id).build();
Member entity = memberService.dtoToEntity(dto);
given(memberRepository.findById(id)).willReturn(Optional.of(entity));
// when
MemberDTO result = memberService.getOne(id);
// then
assertThat(result.getId()).isEqualTo(id);
verify(memberRepository).findById(id);
}
@Test
@DisplayName("회원 조회 실패")
void getOneFail() {
// given
Long id = 0L;
// when
Exception exception = assertThrows(IllegalStateException.class, () -> memberService.getOne(id));
// then
assertEquals("해당 회원이 존재하지 않습니다.", exception.getMessage());
verify(memberRepository).findById(id);
}
}
Mockito가 제공하는 메서드
: 다음 메서드들은 Mock을 사용할 때 실제 메서드가 실행 될 수 없기 때문에 특정 메서드가 실행 됐을 때 리턴값을 설정한다 (= Mock 객체 메서드 설정)
-
when(행위)
: 테스트 행위 정의 [BDD : given()] -
thenReturn(리턴값)
: 주어진 상황에 결과를 설정 [BDD : willReturn()] -
thenThrow(예외)
: 주어진 상황에 발생하는 예외를 설정 -
verify(객체).메소드
: 해당 객체의 메소드가 실행되었는지 검증 [BDD : then().should()]
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// "once"가 1번 add 됐는지 검증
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
verify(mockedList, never()).add("never happened"); // 호출된 적 없음
verify(mockedList, atLeastOnce()).add("three times"); // 최소 한 번
verify(mockedList, atLeast(2)).add("five times"); // 최소 두 번
verify(mockedList, atMost(5)).add("three times"); // 최대 다섯 번
3. Controller 계층 (MVC 테스트)
-
@WebMvcTest
: MVC 슬라이스 테스트-
@WebMvcTest
는@Controller
,@RestController
,@ControllerAdvice
를 읽는다 -
@Service
,@Repository
,@Component
는 읽지 않는다 - excludeFilters 속성을 사용하여 특정 필터를 테스트에서 제외시킬수 있다
-
-
MockMvc
클래스 : MVC 테스트 기능을 제공하는 클래스 -
@MockBean
: 테스트를 위한 가짜 Bean 등록 -
@WithMockUser
:Spring Security
가 제공하는 목유저 테스트- 테스트시 적용할 유저를 Mock으로 만들 수 있다
- username, password, roles 등의 속성이 있다
-
roles
속성 : default-USER
@Controller
public class MemberController {
@Autowired
private MemberService memberService;
@GetMapping("/member")
public String getMemberList(Model model) {
List<Member> members = memberService.getList();
model.addAttribute("members", members);
return "member/list";
}
@GetMapping("/member/signup")
public String signupForm() {
return "members/signup";
}
}
@WebMvcTest(MemberController.class)
public class MemberControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private MemberService memberService;
@Test
@DisplayName("멤버 전체조회")
public void getMemberList() throw Exception {
// 1. given
List<Member> members = new ArrayList<>();
members.add(Member.builder().name("europani").build());
// memberService.getList() 실행되면 위에서 설정한 members 객체를 리턴한다고 설정
given(memberService.getList()).willReturn(members);
// 2. when
mvc.perform(get("/member"))
.andExpect(status().isOk()) // Http Code: 200 테스트
.andExpect(view().name("member/list") // 반환 뷰 테스트
.andExpect(model().attributeExists("members") // model Attribute 프로퍼티 "members" 존재하는지 테스트
.andExpect(model().attribute("members", contains(members)) // "members" 프로퍼티에 members 객체가 존재하는지 테스트
.andDo(print());
// 3. then
verify(memberService).getList(); // memberService.getList() 가 실행됐는지 검증
}
// 권한 테스트 (현재 /member는 ADMIN에게만 허용)
@Test
@WithMockUser
@DisplayName("ROLE:USER 인가 테스트")
public void userRoleUser() throws Exception {
mvc.perform(get("/member/signup"))
.andExpect(status().isForbidden())
.andDo(print());
}
@Test
@WithMockUser(roles = "ADMIN")
@DisplayName("ROLE:ADMIN 인가 테스트")
public void userRoleAdmin() throws Exception {
mvc.perform(get("/member/signup"))
.andExpect(status().isOk())
.andDo(print());
}
}
-
perform(HTTP 메서드)
: HTTP Method 테스트 -
andExpect(기대값)
: 기대값이 나왔는지 체크하는 메서드
API 테스트
@WebMvcTest(EventController.class)
public class EventControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
private EventService eventService;
@Test
@DisplayName("이벤트 1건 조회")
public void getEventTest() throw Exception {
// given
Event event = Event.builder()
.id(1L)
.title("test")
.build();
// when & then
mvc.perform(get("/api/event/{id}", event.getId())
.contentType(MediaType.APPLICATION_JSON)
// .content(objectMapper.writeValueAsString(id))) // Body 필요없음
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE))
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.title").exists())
.andDo(print());
}
}
◼︎ @EnableJpaAuditing
가 사용된 경우
@EnableJpaAuditing // JPA Auditing 활성화
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 메인클래스가 실행되자마자
@EnableJpaAuditing
이 작동한다. 이를 사용하기 위해서는@Entity
가 1개 이상이여야 한다 - 하지만,
@WebMvcTest
는 Controller 관련 클래스만 스캔하기 때문에 JPA 관련 클래스를 읽을 수 없다 -
그 결과
Caused by: java.lang.IllegalArgumentException: JPA metamodel must not be empty!
이 발생한다 - 해결방안 :
@EnableJpaAuditing
를@SpringBootApplication
와 분리
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Configuration
@EnableJpaAuditing // JPA Auditing 활성화
public class JpaConfig {
}
◼︎ @Mock
과 @MockBean
의 차이
-
@Mock
은 mockito 프레임워크가 제공하며 위치는org.mockito
입니다. -
@MockBean
은 스프링 테스트에서 제공하며 위치는org.springframework.boot.test.mock.mockito
입니다.
Bean이 스프링 컨테이너에 존재 할 경우 해당 객체를 Mock으로 주입합니다.