(JPA+Boot)테스트코드 구현
자바 애플리케이션 개발에서 테스트 코드 작성 방법, 코드 커버리지 측정, 도메인/레포지토리/서비스/컨트롤러 계층별 테스트 전략, JUnit5와 AssertJ 활용법, Mock 객체 활용 등을 상세히 다루는 가이드입니다.
(Test) 테스트 코드 작성
코드 커버리지
코드 커버리지 내용이 궁금하다면 참고 링크
-
인텔리J-edit configurations 세팅 및 테스트 실행 때 run test with 커버리지로 실행 ㄱ(run test with 커버리지 이것만해도 바로 됨)
- 참고로 edit configurations에서 Run 멀티 설정, 실행 프로필 설정 등 여러가지 할 수 있음.
-
이클립스도 우클릭>Coverage as>Junit test 로 확인 가능
테스트 코드 작성
보통 메모리DB로 테스트: application.properties 주석 시 boot-data-jpa 의존성이 이를 자동 제공 및 @Entity, @Id, @GeneratedValue로 JPA 어노테이션 사용해야 테이블 생성까지 자동..!
-
print대신 assert 로 비교하자
- 예로
assertEquals
함수 사용시 두개 인자가 “안 동일하면 오류, 동일하면 정상 동작”
- 예로
-
(1) JUnit5
-
Assertions.assertNull, Assertions.assertInstanceOf, Assertions.assertEquals, Assertions.assertThrows
등등 자주 사용
예시 참고 코드
//어떤 객체인지 체크 Assertions.assertInstanceOf(Character.class, character); //결과 비교 - equals Assertions.assertEquals(member, memberRepository.findOne(saveId)); //예외 처리 + 예외 메시지까지 확인법 Throwable exception = Assertions.assertThrows(IllegalStateException.class, () -> { followService.join(findFollow); // 중복검증 예외 발생 }); Assertions.assertEquals("이미 팔로우 요청을 하셨습니다.", exception.getMessage()); log.info("exception.getMessage() : {}", exception.getMessage()); //일부러 예외 터트린 테스트라면 Assertions.fail("중복검증 예외가 발생해야 한다."); //프록시 체크 -> 서비스에 @Transactinal 은 프록시 사용 Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue(); Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
-
-
(2) AssertJ -> Junit5에서 파생된것
-
import static org.assertj.core.api.Assertions.*; assertThat(connection).isNotNull(); assertThat(findMember).isEqualTo(member); assertThatThrownBy(() -> repository.findById(member.getMemberId())) .isInstanceOf(NoSuchElementException.class); // 예외 터지는거 확인
-
테스트 코드 작성 TIP:
-
예외 테스트:
-
(1)try, catch보다 간편하게
assertThrows
를 사용해서 일부러 예외를 터트려서 테스트 -
(2)try, catch대신
@Test(expected = IllegalStateException.class)
를 선언하면 알아서 해당 예외 터질 때 종료해줌- 만약 해당 예외가 안터지면 그다음 코드들이 계속 실행됨. 그 코드는 아래 형태로 작성
-
Assertions.fail("예외가 발생해야 한다.");
예외가 안터져서 오히려 에러 라고 로그를 남겨줌
-
-
스프링 테스트 선언법 (부트기준)
- Junit4 : @RunWith, @SpringBootTest 둘다 선언 - 클래스 단에
- Junit5 : @SpringBootTest 선언 - 클래스 단에 (@RunWith를 자동 제공)
-
@Test 를 메소드마다 선언하여 테스트!! - 메소드 단에
- @RunWith(SpringRunner.class) : 스프링과 테스트 통합 -> Junit4 이하만 사용
-
@SpringBootTest : 스프링 컨테이너와 테스트를 통합 실행. @SpringBootApplication을 찾아서 모든 빈 로드. (이게 없으면 @Autowired 다 실패)
- 레포, 서비스 때 주로 사용하게 됨.
- 컨트롤러만 테스트 방법은 @WebMvcTest 사용 (서블릿 컨테이너 MOCK)
- @SpringBootTest 의 빈 로드를 더 알아보자면,
-
수동 빈 등록(ex:XML)할 때는 메인의 XML파일을 test/resources 하위로 복제하자!
- xml수동 빈 등록은 메인에서 @ImportResource(“classpath:..)”로 직접 가져오는데,
테스트환경에서는 classpath 가 test/resources 경로가 우선이라 못 가져온다.
못 찾으면 main/resources 경로에서 찾는다고는 하던데 본인 경험상 못 찾더라.
- xml수동 빈 등록은 메인에서 @ImportResource(“classpath:..)”로 직접 가져오는데,
- 차라리 @TestConfiguration 테스트에서 스프링 빈 등록을 지원하는 어노테이션을 사용하는것도 있다.
-
수동 빈 등록(ex:XML)할 때는 메인의 XML파일을 test/resources 하위로 복제하자!
-
given, when, then 예시 -> tdd 로 자동완성 사용 중
- given에 멤버 이름 설정
- when에 서비스의 join함수 사용(회원가입 되는지 확인하는 것)
- then에 결과를 보는것. 멤버이름이 잘 생겼는지 등등..(assert보통 씀!)
-
데이터 Init: @PostConstruct나 @BeforEach 등 활용
-
@BeforEach, @AfterEach는 각 @Test 수행 전, 후 동작
- @Test마다 독립적 실행 상태라 필요 (@Test마다 실행해 줌)
- 공통으로 테스트할 객체 생성하기 좋다.
- 다만, DB와 연동하는 레포(DAO)단에선 @Transactional의 @Rollback(false)를 통해 DB의 데이터 공유는 전역으로 가능해서 테스트가 정상 동작 했던 것!
-
InitDB.java
로 개발 모드에서 간편하게 시작과 동시에 데이터를 미리 넣어두려는 목적(테스트 용이)InitDB.java 예시 코드:
실행흐름: InitService 라는 서비스를 inner class 로 간단히 추가 및 빈 등록하고, @PostConstruct 로 바로 실행//안 사용할 거면 스프링 빈 등록 안되게끔(스캔X) @Component 주석 + @PostConstruct 주석 @Slf4j @Component @RequiredArgsConstructor // 생성자 주입 public class InitDB { private final InitService initService; // // 해당 클래스 인스턴스 생성(construct)된 후 자동 실행 @PostConstruct public void init() { initService.initItem(); } // @Service @RequiredArgsConstructor @Transactional // 쓰기모드 -> 바로 DB 저장 public static class InitService { private final EntityManager em; private final ItemRepository itemRepository; public void initItem() { Item item1 = Item.createItem("김익명", "123", "최근에 있었던 대외비", "최근에 이름 들으면 알 만한 회사랑 어떤 프로젝트 협업할 뻔했는데, 중간에 갑자기 뭐가 바껴서 결국 나랑 하기로 한 건 무산되었다. 너무 아까운 일이었는데 대외비라 어디에 이름 까고 말도 못해서 답답했음. 근데 최근에 보니까 그 프로젝트 초대박났더라고 하......^^^^;;; 또 생각해도 개빡치네*발ㅠㅠ", "test.jpeg"); Item item2 = Item.createItem("", "1234", "", "", ""); Item item3 = Item.createItem("철수", "123", "", "테스트", "test.jpeg"); itemRepository.save(item1); itemRepository.save(item2); itemRepository.save(item3); List<Item> items = itemRepository.updateAllNo(); // for(int i= 0 ; i<150; i++) { // String name = "test"+i; // Item item = Item.createItem(name, "12345", "테스트", "테스트123123", ""); // itemRepository.save(item); // } } } }
-
-
@Transactional : 테스트를 실행할 때 마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 (이 어노테이션은 테스트 케이스에서 사용될때만 기본값으로 롤백)
- 롤백을 하기때문에 내부에서 굳이 영속성 컨텍스트 플러시를 안하는 특징을 가짐
- @Rollback(false) : 롤백 취소
- 롤백을 안하니까 자동flush 진행하므로 insert문 로그 확인가능
- em.flush() 함수 사용 : flush 진행
- 롤백은 건드리지않고 수동flush 진행하므로, 롤백은 그대로 진행하면서 insert문 로그 확인가능
-
서비스 TDD 때 트랜잭션 선언안하면 서비스가 동작을 안하기 때문에 그냥 전역으로 써주자! 단,서비스 코드에 트랜잭션 있는건 꼭 확인
- TDD에 선언한 트랜잭션때문에 서비스에 트랜잭션이 없어도 동작하기 때문!
- 참고) 트랜잭션이 겹칠텐데 전파로 인해 기존 사용중인 트랜잭션을 그대로 사용하므로 문제가 없다.
-
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
- @Order()+@Rollback(value=false) 활용 -> 주로 db저장 먼저하려고!
-
@Order(1), @Order(2)
… 로 테스트 코드 실행 순서 지정 가능- 주의 : Order 라는 클래스가 이미 import 중이라면
@org.junit.jupiter.api.Order(1)
- 또한,
org.junit.jupiter.api
패키지의 Order 를 사용한다는 점을 기억
- 주의 : Order 라는 클래스가 이미 import 중이라면
-
@DisplayName
는 간단히 테스트 출력때 항목 이름을 설정해서 출력 가능
-
@Slf4j:
private static final Logger log = LoggerFactory.getLogger(YourClassName.class);
와 같은 로깅 필드가 자동 생성- 덕분에
log.info("{}", member.getId());
바로 사용
- 덕분에
-
컨트롤러”만” 테스트: @WebMvcTest 사용 (서블릿 컨테이너 MOCK)
-> WAS 역할을 톰캣 대신 해주는것!-
@MockBean 으로 가짜 객체 등록. -> 중요: 실제 동작X
- 따라서
when(member.getId()).thenReturn(1L);
이런식으로 임의로 리턴값 지정 -
mockMvc.perform()
사용때 post, get 잘 구분해서 적용 -
Member member = mock(Member.class);
처럼 함수내에서 Mocking 객체도 간단 (@MockBean이랑 동작은 비슷할 듯) - 진짜 컨트롤러 코드만 테스트 하는것!
- 따라서
-
MockMvc의 목적은 3가지
- request 잘 받는지 - content()
- 서비스 메서드로 데이터 잘 전달 되는지 - verify()
- times() 도 같이 많이 사용 (몇번 서비스 호출 되었나 볼 수 있음)
- 왜냐하면 여러번 서비스 불리는 경우 몇번 불리는지 times로 명시해줘야 TooMany 에러 방지
- 서비스 반환값으로 response 잘 받는지 - when, given 필수!
- when, given 으로 해당 서비스의 반환값을 직접 설정가능!
- response(응답) 관련 검증 메소드는 매우 다양!
- andExpect() - status(), view(), redirectedUrl(), model(), content(), jsonPath() 등등.. 응답 관련 메소드..
- andDo() : 요청, 응답관련 전체 메시지 출력
-
로그인 세션 같이 세션 테스트는??
-
MockHttpSession()
활용 -> 가짜 세션으로 로그인 인증 “인터셉터” 통과 -
반대로 세션을 반환하는 “로그인” 컨트롤러 테스트 경우?
-
로그인 로직은 로그인 성공시 세션을 생성! (request 역할)
-
@WebMvcTest 테스트 수행한 로그 request쪽에 session atr 보면 생성된 HttpServletRequest 정보 확인 가능
-
다만, 응답 쿠키는 확인이 불가능했다. 쿠키는 웹에서는 자동 처리해주다보니 환경 차이인 것 같다.
-
-
-
아래는 참고 메서드
Mock관련 메서드들 참고
- perfom()
- HTTP 요청을 할 수 있다.
- 체이닝이 지원되어 여러 검증 기능을 이어서 선언할 수 있다.
- HTTP 요청을 할 수 있다.
- andExcept()
- mvc.perform의 결과를 검증한다.
- status() : 상태 코드를 검증할 수 있다.
- view() : 리턴하는 뷰에 대해 검증한다.
- ex) andExcept(view().name(“/detailpage”))
- ex) andExcept(view().name(“/detailpage”))
- redirectedUrl() : 리다이렉트 url을 검증한다.
- ex) redirectedUrl(“/where”);
- ex) redirectedUrl(“/where”);
- model() : 컨트롤러의 Model에 관해 검증한다.
- ex) model().attribute(“alcoholDetails”, alcoholDetails)
- ex) model().attributeHasFieldErrorCode(“loginForm”, “userId”, “NotBlank”)
- ex) model().attribute(“alcoholDetails”, alcoholDetails)
- content() : 응답에 대한 정보를 검증한다.
- jsonPath() : response body에 들어갈 json 데이터를 검증한다.
- ex) jsonPath(“$.status”).value(“400”)
- ex) jsonPath(“$.status”).value(“400”)
- mvc.perform의 결과를 검증한다.
- andDo()
요청/응답 전체 메세지를 확인할 수 있다.
- 추가 참고
- Mock() - 모의 객체를 생성하는 역할
- when() - 협력객체 메소드 반환 값을 지정해주는 역할(stub)
- verify() - stub안의 협력객체 메소드가 호출 되었는지 확인
- times() - 지정한 횟수 만큼 협력 객체 메소드가 호출 되었는지 확인
- never() - 호출되지 않았는지 여부 검증
- atLeastOnce() - 최소 한 번은 특정 메소드가 호출되었는지 확인
- atLeast() - 최소 지정한 횟수 만큼 호출되었는지 확인
- atMost() - 최대 지정한 횟수 만큼 호출되었는지 확인
- clear() - stub을 초기화 한다
- timeOut() - 지정된 시간 안에 호출되었는지 확인
- Mock() - 모의 객체를 생성하는 역할
- perfom()
-
Request 관련 테스트: HttpServletRequest, HttpServletResponse ->
MockHttpServletRequest, MockHttpServletResponse
을 사용! (아직 사용해보지는 않았음) -
NoSuchBeanDefinitionException 에러주의) 컨트롤러에서 사용한 레포,서비스 등등 은 꼭 Mock해주자. 테스트코드에는 없고 컨트롤러에서만 사용중이어도 무조건 @MockBean 등록은 일단 필수다!!
- 예로 @Test 회원가입() 로직에 memberService만 사용하고 exp, character서비스는 사용하지 않았다.
- 근데, 실제 register() 로직인 “회원가입 컨트롤러 메서드” 코드를 보면 exp, character서비스 전부 사용한다.
- 그렇다면 아래처럼 @MockBean 등록은 일단 필수다!!
-
mockMvc.perform
함수에서 json 내용을 체크 하고 싶을때? ->jsonPath()
- 배열 처리는?
.andExpect(jsonPath("$[0].content").value("알림테스트1")) // 응답 body 의 json 확인 .andExpect(jsonPath("$[2].content").exists()) .andExpect(jsonPath("$[3]").doesNotExist()) // 없어야 정상
- 일반 적인 {}는?
.andExpect(*jsonPath*("$.data.uid").value("12345")) // 응답 body 의 json 확인
- 배열 처리는?
-
예시 코드 참고
도메인
@Slf4j
class MemberTest {
@Test
public void 생성_편의메서드() throws Exception {
// given
Member member = null;
// when
member = Member.createMember("test", "테스트1");
// then
Assertions.assertInstanceOf(Member.class, member);
}
//
@Test
public void 연관관계_편의메서드() throws Exception {
// given
Member member = Member.createMember("test", "테스트2");
Lists lists = Lists.createLists(member, LocalDateTime.now(), new ArrayList<>());
log.info("{}", member.getLists().size()); //=0
// when
member.addLists(lists);
log.info("{}", member.getLists().size()); //=1
// then
Assertions.assertEquals(member.getLists().size(), 1);
}
}
레포지토리
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
@Slf4j
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
//
/**
* save, findOne, findByUid, findAllWithPage
*/
@Test
@Order(1)
@Transactional // 롤백
@Rollback(value = false)
public void 회원가입_조회() throws Exception {
// given
Member member = Member.createMember("test", "test1");
// 혹시모를 FK 에러 방지
Exp exp = Exp.createExp(0L, 0L, 1L);
em.persist(exp); // FK id 위해
Character character = Character.createCharacter(exp, new ArrayList<>(), new ArrayList<>(),
new ArrayList<>());
em.persist(character); // FK id 위해
member.setCharacter(character);
//
// when
memberRepository.save(member); // persist
log.info("{}", member.getId());
// em.flush(); // 롤백 true 때문에 insert 쿼리 생략시 flush 추가로 볼 수 있음
// em.clear(); // flush 사용시 이것까지 해줘야 아래 select 문 전송 쿼리도 볼 수 있음
Member findMember = memberRepository.findOne(member.getId());
log.info("{}", member.getId());
//
// then
Assertions.assertEquals(member.getId(), findMember.getId());
// 단, 위에서 em.clear를 한 경우 영속성이(캐시) 비어있으므로 findMember가 새로운 주소!!
// 따라서 아래 출력으로 틀리다는 결론이 나온다.
Assertions.assertEquals(member, findMember);
}
//
@Test
@Order(2)
@Transactional
public void 회원조회_UID() throws Exception {
// given
Member findMember;
// when
findMember = memberRepository.findByUid("test");
Member findMember2 = memberRepository.findByUid("testtest");
// then
Assertions.assertEquals(findMember.getNickname(), "test1");
Assertions.assertEquals(findMember2, null);
}
//
@Test
@Order(3)
@Transactional
public void 회원조회_Page() throws Exception {
// given
List<FindMemberResponseDto> memberList = new ArrayList<>();
// when
memberList = memberRepository.findAllWithPage(1); // order by desc
log.info("memberList : {}", memberList.size());
log.info("memberList.get(0) : {}", memberList.get(0));
// then
for (FindMemberResponseDto dto : memberList) {
log.info("member id : {}, nickname : {}", dto.getId(), dto.getNickname());
}
Assertions.assertEquals(memberList.get(0).getNickname(), "test1");
}
}
서비스
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest
@Transactional // 쓰기모드 -> 서비스코드에 트랜잭션 유무 반드시 확인
@Slf4j
public class MemberServiceTest {
@Autowired
EntityManager em;
@Autowired
MemberService memberService;
static final String UID = "12345";
static final String MESSAGE = "이미 존재하는 회원입니다.";
static Long memberId;
//
/**
* join(중복검증 포함), findOne, findByUid, {findAllWithPage, initCacheMembers}(=회원 최신순_페이징 조회+캐시)
*
@Test
@Order(1)
@Rollback(value = false)
public void 회원가입_조회() throws Exception {
// given
Member member = Member.createMember(UID, "테스트 닉네임");
Exp exp = Exp.createExp(0L, 0L, 1L);
em.persist(exp);
Character character = Character.createCharacter(exp, new ArrayList<>(), new ArrayList<>(),
new ArrayList<>());
em.persist(character);
member.setCharacter(character);
// when
memberService.join(member);
Member findMember = memberService.findOne(member.getId());
Member findMember2 = memberService.findByUid(UID);
// then
Assertions.assertEquals(member.getId(), findMember.getId());
Assertions.assertEquals(member.getId(), findMember2.getId());
memberId = member.getId();
}
//
@Test
@Order(2)
public void 중복검증_예외() throws Exception {
// given
Member member = memberService.findOne(memberId);
// when
// then
Throwable exception = Assertions.assertThrows(IllegalStateException.class, () -> {
memberService.join(member); // 예외발생 로직
});
Assertions.assertEquals(MESSAGE, exception.getMessage());
log.info("exception.getMessage() : {}", exception.getMessage());
}
//
@Test
@Order(3)
public void 회원_페이징_캐시_조회() throws Exception {
// given
// when
List<FindMemberResponseDto> dto = memberService.findAllWithPage(1);
log.info("캐시되었으면 쿼리 안날라감1");
memberService.findAllWithPage(1);
log.info("캐시되었으면 쿼리 안날라감2");
memberService.findAllWithPage(1);
log.info("캐시되었으면 쿼리 안날라감3");
memberService.initCacheMembers(); // 캐시 초기화
log.info("캐시 초기화 했으므로 쿼리 날라가야 함");
memberService.findAllWithPage(1);
// then
for (FindMemberResponseDto m : dto) {
log.info("member.id : {}", m.getId());
log.info("member.nickName : {}", m.getNickname());
}
}
}
컨트롤러
@WebMvcTest(controllers = MemberApiController.class)
class MemberApiControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean // 가짜 객체이므로 실제 동작은 X
ExpService expService;
@MockBean
CharacterService characterService;
@MockBean
MemberService memberService;
//
/**
* login, register, logout, findAllWithPage
*/
@Test
public void 회원가입() throws Exception {
// given
String content = "{\"uid\":\"12345\", \"nickname\":\"회원가입 테스트\"}"; //입력 json 흉내
// Member member = Member.createMember("12345", "회원가입 테스트");
// member.setId(1L);
// Mocking Member 객체
Member member = mock(Member.class);
when(member.getId()).thenReturn(1L); // ID 반환 설정
when(member.getUid()).thenReturn("12345");
when(member.getNickname()).thenReturn("회원가입 테스트");
// when
when(memberService.join(any())).thenReturn(member);
mockMvc.perform(
post("/api/v1/members/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.content(content)
)
.andExpect(status().isCreated()) // 예상 응답
.andExpect(jsonPath("$.data.uid").value("12345")) // 응답 body 의 json 확인
.andDo(print());
// then
verify(memberService).join(any());
verify(expService).join(any());
verify(characterService).join(any());
}
//
@Test
public void 로그인() throws Exception {
// given
String content = "{\"uid\":\"123\"}"; // {"name":"value"} 형태로 작성해야 JSON 형태!
// Member member = Member.createMember("123", "test");
// member.setId(1L);
// Mocking Member 객체
Member member = mock(Member.class);
when(member.getId()).thenReturn(1L); // ID 반환 설정
when(member.getUid()).thenReturn("123");
when(member.getNickname()).thenReturn("로그인 테스트");
// when
when(memberService.findByUid(any())).thenReturn(member);
mockMvc.perform(
post("/api/v1/members/login")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
.content(content)
)
.andExpect(status().isOk()) // 예상 응답
.andDo(print());
// then
verify(memberService).findByUid(any());
}
//
@Test
public void 로그아웃_성공과중복() throws Exception {
// given
// Member member = Member.createMember("123", "test");
// member.setId(1L);
// Mocking Member 객체
Member member = mock(Member.class);
when(member.getId()).thenReturn(1L); // ID 반환 설정
when(member.getUid()).thenReturn("123");
when(member.getNickname()).thenReturn("로그아웃 테스트");
MockHttpSession session = new MockHttpSession();
session.setAttribute(SESSION_NAME_LOGIN, member.getId());
// when
mockMvc.perform(
post("/api/v1/members/logout")
.session(session)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
)
.andExpect(status().isOk()); // 로그아웃 성공
mockMvc.perform(
post("/api/v1/members/logout")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
)
.andExpect(status().isConflict()); // 로그아웃 중복
// then
}
//
@Test
public void 멤버_조회_페이징() throws Exception {
// given
int pageId = 1;
MockHttpSession session = new MockHttpSession();
session.setAttribute(SESSION_NAME_LOGIN, 1); // 회원 인증 인터셉터 통과위해
// 테스트용 members 세팅
List<FindMemberResponseDto> members = new ArrayList<>();
FindMemberResponseDto m1 = new FindMemberResponseDto(111L, "테스트1", 1L);
FindMemberResponseDto m2 = new FindMemberResponseDto(222L, "테스트2", 1L);
FindMemberResponseDto m3 = new FindMemberResponseDto(333L, "테스트3", 1L);
members.add(m1);
members.add(m2);
members.add(m3);
// when
when(memberService.findAllWithPage(1)).thenReturn(members);
mockMvc.perform(
get("/api/v1/members/" + pageId)
.session(session)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8")
)
.andExpect(status().isOk()) // 예상 응답
.andDo(print());
// then
verify(memberService).findAllWithPage(1);
}
}
페이징 테스트 코드는 “MyBatis + Spring 파트” 게시물 참고
댓글남기기