아으... 어제 썻던 글들이 모두 날라가서 점심먹기 전 후다닥 작성하려고 한다.
지난번 pr 에 이렇게 리뷰를 받았다, ㅎㅎ 생각해보니 멤버가 인자에 포함되어 있다면 본인 이 쓴 댓글에 만 좋아요와 좋아요 취소가 가능해지는 바보같은 실수를 해버렸지 뭔가 ㅎㅎㅎ ...
바로 피드백 반영해서 추가하고 머지 했다.
어제 오전 오후 는 서버 띄워서 완전 Api 테스트 하느라 시간을 보냈다. 자바버전 이 달라서 생기는오류, 각각 db ddl 이 달라서 생기는 오류 등 파일의 이전에 따른 querydsl 컴파일 미실시 등 다양한 에러를 경험하며 어제 동료분 에게 메세지 엄청 보냈던것 같다. ㅎㅎㅎ
저런 자잘한 오류들 을 해결하고 나니 시간이 훌쩍 가있지 않은가..
바로 일정 관련 부분 오늘은 한번에 작성할 생각이다.
이렇게 보면 도대체 리스트 와 달력 의 구분이 왜 필요한가 애초에 줄때 그냥 던져주면 안되는건가 ? 라고 생각할수도 있지만, 우리의 미리 만들어진 와이어프레임을 본다면
이렇게 빨간색으로 점쳐진 부분이 보이는가 ? 그부분 을 한달치 달력으로 표현을 하고자 한다. 그렇기에 이렇게 따로 api 구분을 해놨다.
바로 테스트 코드 부터 들어가 보자.
@Nested
@DisplayName("멤버 월 일정 API 테스트")
class CalendarMonthTest{
@Test
@DisplayName("멤버 월 일정 API 실패 [날짜 데이트 이상할시]")
public void fail() throws Exception{
//given
//when
mockMvc.perform(get("/api/v1/member/calendar?date=2022")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value("F"))
.andDo(print());
}
@Test
@DisplayName("멤버 월 일정 API 성공")
public void success() throws Exception{
//given
//when
mockMvc.perform(get("/api/v1/member/calendar?date=2022-09-29")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andDo(print());
;
//then
}
}
날짜 관련 부분 에 있어 잘못된 데이터 를 받을 때 테스트를 위한 총 2가지를 작성하였다.
이렇게 테스트 를 작성할때 webmvctest 를 작성해야할지 아니면 springboottest 를 할지 고민이 많을것이다.
이둘은 분명한 차이가 있으니 명확히 하고 지나가자
바로 스프링 공홈 으로 가보자.
The
@SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with
@SpringBootApplication
, for instance) and use that to start a Spring application context. You can run this test in your IDE or on the command line
스프링 부트 테스트 어노테이션 은 스프링 부트 의 기본구성 을 찾고 , 스프링 어플리케이션 컨텍스트 를 실행할때 사용한다고 한다
응 뭔소리인가 ?
SpringApplication을 통해 테스트에 사용되는 ApplicationContext를 생성하여 작동한다 라고 나와있다 이말은 즉슨 스프링실행 및 구성 을 위해 필요한 빈들을 모조리 등록해서 테스트에 임한다고 생각하면 된다.
그렇다면 WebMvcTest 는 ?
If you want to focus only on the web layer and not start a complete ApplicationContext, consider using @WebMvcTest instead.
설명을 타고 내려가다 보면 이렇게 명시 되어 있다. 완저히 어플리케이션을 실행하지 않고 웹레이어 에 초점을 맞추고 싶다면, mvcTest 를 고려하라고 말이다.
@WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller,
@ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, and HandlerMethodArgumentResolver
이렇게 나와 있다. 컨트톨러,어드바이스,제이슨 컴포넌트 ,컴버터 드등 이 제한적으로 스캔되어 스프링 어플리 케이션을 구성한다고 나온다. 이말은 다시말해 스프링 부트 테스트 와달리 테스트 에 필요한 최소한의 빈 스캔만 한다고 생각하면 된다.
이에 필요한 빈들이 있다면 @MockBean 을 이용해 빈을 대체 해주어 컨트롤러 본연 만 테스트 를 하면 된다.
이렇게 보면 단점 과 장점이 한눈에 들어올 것이다.
webmvctest 는 빠르다 대신 모든 테스트가 통과한다고 해서, 실제 모든빈이 등록된 부트 의 어플리케이션 상에서 오류 를 야기할수있다.
springboottest 는 실제 모든 빈을 등록 해 실행 하기 때문에 실제 프로덕트 환경과 가장 유사한 테스트 를 제공하나 , 시간이 오래걸릴수 있다.
본인이 추가하고자 하는 테스트 코드를 작성해서 UnitTesting을 할지 e2e테스트 를 할지 정해서 하면 될것같다.
두개 모두 필요한 테스트 이다.
이런식의 로컬데이트 로 데이터를 받아오는 컨트롤러 뭐 특별할것 없다. 주석으로 예시를 적어 보다 직관적으로 만들어 주어야겠다.
과정을 쓰고싶었는데 어제 날라 가는 바람에 이렇게 결과물 밖에 보여줄수가 없다 .. ㅠ
유저 의 이메일을 가져오고 상태값을 비교하기 위해 저렇게 함수로 빼주었다. 다음 서비스 함수에서도 사용 될거 같아 저렇게 작성했다.
데이트 의 형태가 "2022-09-30" 이러한 형식으로 오기 때문에 , 월별로 데이터를 얻기위해 파싱을 진행했다. 추후 함수 로 빼던가 해서 보다 직관적인 형태로 만들어볼까 생각중이다.
테스트 코드는 아래와 같이 작성하였다.
@Nested
@DisplayName("멤버 일정 월 서비스 테스트")
class month{
@Test
@DisplayName("멤버 일정 월 서비스 실패 [로그인 유저 미일치]")
public void fail1() throws Exception{
//given
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_EMAIL_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 실패 [정지된 회원]")
public void fail2() throws Exception{
Member m = Member.builder().memberStatus(MemberStatus.MEMBER_STATUS_STOP).build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_STOP_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 실패 [탈퇴한 회원]")
public void fail3() throws Exception{
Member m = Member.builder().memberStatus(MemberStatus.MEMBER_STATUS_WITHDRAW).build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getMonthCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_WITHDRAW_ERROR,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 성공")
public void success() throws Exception{
Member m = Member.builder().build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
doReturn(List.of("2022-09-01","2022-09-30")).when(memberCalendarQueryRepository)
.searchMemberMonthDate(any(), any(),any());
//when
List<String> monthCalendar = memberCalendarService.getMonthCalendar(LocalDate.now());
//then
verify(memberCalendarQueryRepository,times(1))
.searchMemberMonthDate(any(),any(),any());
assertThat(monthCalendar.size()).isEqualTo(2);
}
}
1~3번 은 지난번 수 많은 테스트 의 반복으로 숙달 되었을 것이다. 성공 테스트를 보자.
memberCalendarQuery 를 이용해 dto 없이 바로 뿌려준다. 이유는 바로 데이터 별로 필터를 해서 들고 오는데 굳이 dto 가 필요한가 생각해서 그냥 저렇게 작성해주었다.
테스트는 한번의 실행됨 을 체크하고 반환값의 사이즈를 측정하였다.
이 몇줄 안되는 쿼리를 작성 하는라 고민을 조금 많이 했다. 날짜부터 시작해서 이것저것 테스트를 해본후에 이렇게 작성할수 있었다.
월별로 데이터 만 뿌려서 빨간점을 찍어주면 되기때문에 그룹바이 로 중복 처리를 해버렸다.
또한 string 타입을 groupby orderby 쪽에 작성을 바로 못하니 저렇게 stringpath 를 이용해 작성해 넘겨주자.
select
DATE_FORMAT(groupmeeti1_.meeting_start_date,
?) as col_0_0_
from
member_calendar membercale0_
inner join
group_meeting groupmeeti1_
on membercale0_.group_meeting_id=groupmeeti1_.group_meeting_id
where
membercale0_.member_id=?
and membercale0_.meeting_active_status=?
and (
groupmeeti1_.meeting_start_date between ? and ?
)
group by
DATE_FORMAT(groupmeeti1_.meeting_start_date,
?)
크 생각한 대로 쿼리가 날아간다 만족 스럽다.
테스트 도 만족 스럽게 통과 된다. 점심먹고 이어서 작성해보자.
자 다음 일정 목록을 위한 테스트 코드를 작성해보자.
노션 상에 일자를 받지 않아서 이렇게일자를 받는 방식으로 변경해 주었다.
통과 되기위해 컨트롤러 코드를 작성해보자.
@GetMapping("/list")
public ApiResult<?> getDayCalendar(
// date type example "2022-09-30"
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Valid LocalDate date
){
// memberService.getDayCalendar(date);
return ApiUtils.success(null);
}
바로 통과 된다 기분이 매우좋다 ㅎㅎ
서비스 테스트 코드를 작성하러 가보자.
위의 서비스와 동일한 테스트에 한가지를 더 추가 하였다 물론 프론트 에서 달력 을 클릭할때 일정이 있는경우만 클릭하게 만들수 있으나, 2번 체크해서 나쁠거 없다고 생각해서 위와같은 방어선을 하나 더 추가했다 일정이 없는경우 에러를 터트려 400을 리턴할 생각이다.
아직 서비스가 만들어 지지않아 저렇게 빨간 불이 들어온다.
지난번 만든 함수 get member 를 이용해서 1~3번 까지 의 모든 테스트 를 통과 해 주자.
4번 과 5번의 테스트 를 진행하기 전에 먼저 프론트에서 어떻게 데이터를 받아야 보다 편하게 다음 페이지로 넘겨줄수 있을까 에 대해 고민을 해보자.
1. 그룹미팅 아이디 가 포함되어 있어야 디테일을 확인할때 Api 를 쏴서 편하게 확인할수 있다.
2. 그룹 의 이름 정도는 표기 해 주는게 맞지 않겠는가 이름 도 넣어주자.
3. 시간 이 제일 중요하다 시간에 따라 표기 true flase 를 이용해 화면에 랜더링 해줄테니
요렇게 3개의 데이터만 넘겨주면 되니 이걸 위한 dto 를 작성해주자.
@Getter
@Setter
@Builder
@NoArgsConstructor
public class CalendarDayListDto {
private Long groupMeetingId;
private String groupMeetingTitle;
private LocalDateTime startDate;
private LocalDateTime endDate;
@QueryProjection
public CalendarDayListDto(Long groupMeetingId, String groupMeetingTitle, LocalDateTime startDate, LocalDateTime endDate) {
this.groupMeetingId = groupMeetingId;
this.groupMeetingTitle = groupMeetingTitle;
this.startDate = startDate;
this.endDate = endDate;
}
}
Dto 를 작성하다 보니 시간 의 범위를 주어야 프론트에서 랜더링 하기가 편하다 그래서 이렇게 End 도 하나 추가 해 주었다.
테스트 코드 작성을 마무리 해보자.
@Test
@DisplayName("멤버 일정 일 서비스 실패 [일정이 없는 경우]")
public void fail4() throws Exception{
Member m = Member.builder().build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
doReturn(null).when(memberCalendarQueryRepository)
.searchMemberDayDate(any(), any(),any());
//when
MemberException exception = assertThrows(
MemberException.class,
()->memberCalendarService.getDayCalendar(LocalDate.now()));
//then
assertEquals(MEMBER_CALENDAR_DOES_NOT_EXIST,exception.getErrorCode());
}
@Test
@DisplayName("멤버 일정 월 서비스 성공")
public void success() throws Exception{
Member m = Member.builder().build();
CalendarDayDto d = CalendarDayDto.builder().build();
CalendarDayDto d2 = CalendarDayDto.builder().build();
//given
given(memberRepository.findByEmail(any()))
.willReturn(Optional.of(m));
doReturn(List.of(d,d2)).when(memberCalendarQueryRepository)
.searchMemberDayDate(any(), any(),any());
//when
List<CalendarDayDto> dayCalendar = memberCalendarService.getDayCalendar(LocalDate.now());
//then
verify(memberCalendarQueryRepository,times(1))
.searchMemberDayDate(any(),any(),any());
assertThat(dayCalendar.size()).isEqualTo(2);
}
레포지토리 마무리 하러 가보자.
dto 로 바로 받고 싶어서 querydsl 사용 했다 저 where 부분이 겹치니 함수로 따로 만들어주자.
public List<CalendarDayDto> searchMemberDayDate(Long memberId, LocalDateTime startDate, LocalDateTime endDate){
return jpaQueryFactory.select(
new QCalendarDayDto(
groupMeeting.id,
groupMeeting.title,
groupMeeting.meetingStartDate,
groupMeeting.meetingEndDate
)
)
.from(memberCalendar)
.join(memberCalendar.groupMeeting, groupMeeting)
.where(
validateMemberCalendar(memberId,startDate,endDate)
)
.orderBy(groupMeeting.meetingStartDate.asc())
.fetch();
}
private BooleanExpression validateMemberCalendar(Long id,LocalDateTime start,LocalDateTime end){
return memberIdEq(id).and(meetingActiveStatus().and(betweenDate(start,end)));
}
private BooleanExpression memberIdEq(Long id){
return memberCalendar.member.id.eq(id);
}
private BooleanExpression meetingActiveStatus(){
return memberCalendar.meetingActiveStatus.eq(MEETING_ACTIVE_STATUS_ING);
}
private BooleanExpression betweenDate(LocalDateTime start,LocalDateTime end){
return memberCalendar.groupMeeting.meetingStartDate.between(start, end);
}
서비스 코드를 마무리 해보자.
테스트 는 모두 통과 된다 ㅎㅎ
사실 중간에 Null 리턴해서 nullPoitner 예외 터져서 비어있는 리스트로 변경해주었다... ㅎㅎ
서버를 띄우고 테스트를 해보자.
모두 만족스럽게 나간다 다만 시간 부분에 있어 포맷팅을 조금 해주어야 프론트가 편할꺼 같으니 포메팅을 하러가자.
@Data
@Builder
public class CalendarDay {
private Long groupMeetingId;
private String groupMeetingTitle;
private String startDate;
private String endDate;
public static List<CalendarDay> from(List<CalendarDayDto> res){
return res.stream().map(
(i)->CalendarDay.builder()
.groupMeetingId(i.getGroupMeetingId())
.groupMeetingTitle(i.getGroupMeetingTitle())
.startDate(dateParseToString(i.getStartDate()))
.endDate(dateParseToString(i.getEndDate()))
.build()
).collect(Collectors.toList());
}
private static String dateParseToString(LocalDateTime time){
// 정책 에 따라 파싱 하는 방법이 달라져야 한다.
int hour = time.getHour();
int minute = time.getMinute();
if(minute > 0) hour++;
StringBuilder sb = new StringBuilder();
if(hour < 10){
sb.append("0").append(hour);
}else{
sb.append(hour);
}
sb.append(" : ").append("00");
return sb.toString();
}
}
클래스하나 더만들어서 포맷팅 해주었다 현재 시간 단위로만 예약이 가능하기 떄문에 이렇게 작성을 한것이고 추후 서비스 정책이 바뀐다면 이 파싱하는 함수 또한 바뀌어야할것이다.
최종 이렇게 리턴을 해주게 된다. ㅎㅎ
긴글 읽어주셔서 감사합니다.
'사이드 프로젝트 > 워크듀오-개발일지' 카테고리의 다른 글
Token 암호화 활용해보기 (해쉬, 대칭 암호화,비대칭 암호화) (1) | 2024.01.19 |
---|---|
[리팩토링] 멀티모듈(1일차,그래들 설정) (2) | 2023.01.26 |
멤버 피드 댓글 삭제 API (1) | 2022.09.28 |
멤버 피드 댓글 업데이트 API (0) | 2022.09.28 |
멤버 피드 댓글 조회 API (1) | 2022.09.28 |