Hyun's Wonderwall

[GDSC Study] 스프링 기반 REST API 개발 - 2주차 | 섹션 2. 이벤트 생성 API 개발 본문

Study/Java, Spring

[GDSC Study] 스프링 기반 REST API 개발 - 2주차 | 섹션 2. 이벤트 생성 API 개발

Hyun_! 2023. 11. 14. 23:46

GDSC Ewha 5기_ Spring Boot 스터디

  • 스터디 커리큘럼: 백기선, "스프링 기반 REST API 개발"
  • 2주차 과제: 섹션 2. 이벤트 생성 API 개발
    2.(1) 이벤트 API 테스트 클래스 생성
    2.(2)  201 응답 받기
    2.(3)  이벤트 Repository
    2.(4)  입력값 제한하기
    2.(5)  입력값 이외에 에러 발생
    2.(6)  Bad Request 처리
    2.(7)  Bad Request 응답
    2.(8)  비즈니스 로직 적용
    2.(9)  매개변수를 이용한 테스트

2. (1) 이벤트 API 테스트 클래스 생성 - Event 도메인 구현

입력값 필드 생성 : 

public class Event {

    private String name;
    private String description;
    private LocalDateTime beginEnrollmentDateTime;
    private LocalDateTime closeEnrollmentDateTime;
    private LocalDateTime beginEventDateTime;
    private LocalDateTime endEventDateTime;
    private String location; // (optional) 이게 없으면 온라인 모임
    private int basePrice; // (optional)
    private int maxPrice; // (optional)
    private int limitOfEnrollment;

}

여기에서 추가 필드 생성 : (입력값x, 결과값o)

    private Integer id;
    private boolean offline;
    private boolean free;
    private EventStatus eventStatus = EventStatus.DRAFT;

EventStatus 이늄(enum) 추가

public enum EventStatus {
    DRAFT, PUBLISHED, BEGAN_ENROLLMEND, CLOSED_ENROLLMENT, STARTED, ENDED
}

롬복 애노테이션 추가

@Getter @Setter @EqualsAndHashCode(of = "id")
@Builder @NoArgsConstructor @AllArgsConstructor
public class Event { ... }

 

# Questions (1~4)

1. 왜 @EqualsAndHasCode에서 of를 사용하는가

이 애노테이션은 equals()와 hashCode()를 생성함

  • equals() : 객체의 주소값을 비교하여 일치여부를 반환한다. (boolean값을 리턴. 객체 동일성identity 비교)
  • hashCode() : 객체의 주소값을 이용하여 객체 고유의 해시코드를 리턴한다.

🔗[Java] equals와 hashCode 함수 - MangKyu's Diary https://mangkyu.tistory.com/101

String, Integer클래스에서 쓰이는 equals()는 오버라이딩(재정의) 된 것으로 객체의 내용(value)을 비교한다. 기능이 다름!

🔗 [Java] 객체비교시 왜 equals()와 hashcode() 둘 모두를 재정의해야 하는가?  https://velog.io/@mooh2jj/equals%EC%99%80-hashCode%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80

==와도 비교해보기

🔗[Java] equals, ==, hashCode 의 차이와 재정의 시점에 대해 — 파즈의 공부 일기 https://bepoz-study-diary.tistory.com/254

 

2. 왜 @Builder를 사용할 때 @AllArgsConstructor가 필요한가

private 필드들을 참조할 수 있어야 하므로 전체 필드를 갖는 생성자가 필요하다. 이 @AllArgsConstructor으로 전체 필드를 갖는 생성자를 생성해줘야 한다.

🔗 Builder 패턴, @NoArgsConstructor, @AllArgsConstructor https://ejejc.tistory.com/27

(상세 설명)
자바에서는 파라미터가 존재하는 생성자를 만들 경우 반드시 파라미터가 없는 기본 생성자를 작성해야 한다.
@NoArgsConstructor : 파라미터가 없는 기본 생성자 생성 (코드에 직접 명시 안해도 됨)
→기본 생성자를 만드는게 좋다. 근데 이걸 쓰면 이미 기본 생성자가 존재하기 때문에, 자동으로 다른 생성자가 생성되지 않는다. 또한 초기화해줘야 하는 필드들 초기화되지 않으면 컴파일 에러 (final이나 @NonNull)
+ @RequiredArgsConstructor : final이나 @NonNull으로 선언된 필드만을 파라미터로 받는 생성자를 생성. 클래스가 의존 하는 필드 간단하게 초기화가능.

🔗 롬복 @All/NoArgsConstructor 제대로 알고 사용해보자. https://velog.io/@code-10/%EB%A1%AC%EB%B3%B5-AllNoArgsConstructor-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90

@AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자 생성. (@NonNull 포함, null check 코드 포함)

🔗 [Lombok]@Builder과 생성자 애노테이션 :: 당근케잌 https://yeon-kr.tistory.com/176

 

3. @Data를 쓰지 않는 이유

@EqualsAndHashCode를 of="id"로 해주기 위해서..? (확인 필요)

4. 애노테이션 줄일 수 없나
아직은~ㅠ

테스트 만들자

스프링 부트 슬라이스 테스트

- 슬라이스 테스트란? 애플리케이션을 특정 레이어(계층)로 쪼개어 독립적으로 테스트. 

🔗 Testing https://velog.io/@minthug94_/Testing (단위 테스트, 슬라이스 테스트, 통합 테스트, 기능 테스트가 있다)

  • @WebMvcTest : 웹 애플리케이션의 MVC 컨트롤러를 테스트하기 위해 사용. (웹 관련 빈만 등록해 준다-슬라이스)
    MockMvc 빈을 자동 설정 해주는 특징. (따라서 그냥 가져와서 쓰면 됨)
  • MockMvc : 스프링 MVC 테스트 핵심 클래스. 웹 서버를 띄우지 않고도 스프링 MVC(DispatcherServlet)가 요청을 처리하는 과정을 확인할 수 있어 Controller 테스트용으로 자주 쓰임.
    +) DispatcherServlet (디스패처 서블릿)이란? : HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러.(Front Controller)
    🔗 [Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정 - MangKyu's Diary https://mangkyu.tistory.com/18
    🔗 DispatcherServlet이란?, Spring MVC의 핵심 https://lcs1245.tistory.com/entry/DispatcherServlet%EC%9D%B4%EB%9E%80-Spring-MVC%EC%9D%98-%ED%95%B5%EC%8B%AC

< 테스트 할 것 >

  • 입력값들을 전달하면 JSON 응답으로 201이 나오는지 확인.
    - Location 헤더에 생성된 이벤트를 조회할 수 있는 URI 담겨 있는지 확인.
    - id는 DB에 들어갈 때 자동생성된 값으로 나오는지 확인
  • 입력값으로 누가 id나 eventStatus, offline, free 이런 데이터까지 같이 주면?
    - Bad_Request로 응답 vs 받기로 한 값 이외는 무시
  • 입력 데이터가 이상한 경우 Bad_Request로 응답
    - 입력값이 이상한 경우 에러
    - 비즈니스 로직으로 검사할 수 있는 에러
    - 에러 응답 메시지에 에러에 대한 정보가 있어야 한다.
  • 비즈니스 로직 적용 됐는지 응답 메시지 확인
    - offline과 free 값 확인
  • 응답에 HATEOA와 profile 관련 링크가 있는지 확인.
    - self (view) / - update (만든 사람은 수정할 수 있으니까) / - events (목록으로 가는 링크)
  • API 문서 만들기
    - 요청 문서화 / - 응답 문서화 / - 링크 문서화 / - profile 링크 추가

2. (2)  201 응답 받기 

@RestController : @ResponseBody를 모든 메소드에 적용한 것과 동일하다.

ResponseEntity를 사용하는 이유 : 응답 코드, 헤더, 본문 모두 다루기 편한 API

Location URI 만들기 : HATEOS가 제공하는 linkTo(), methodOn() 사용 -> 나중에 바꿈

객체를 JSON으로 변환 : ObjectMapper 사용

<테스트 할 것>

입력값들을 전달하면 JSON 응답으로 201이 나오는지 확인.
Location 헤더에 생성된 이벤트를 조회할 수 있는 URI 담겨 있는지 확인.
id는 DB에 들어갈 때 자동생성된 값으로 나오는지 확인.


2. (3) 이벤트 Repository 구현

EventRepository : 스프링 데이터 JPA의 JpaRepository 상속 받아 만들기 (아래에 코드)

Enum을 JPA 맵핑시 주의할 것 : @Enumerated(EnumType.STRING) 애노테이션을 eventStatus에 붙임

@Enumerated(EnumType.STRING)
private EventStatus eventStatus;

 

@MockBean : Mockito를 사용해서 mock 객체를 만들고 빈으로 등록해 줌.
(주의) 기존 빈을 테스트용 빈이 대체한다

@MockBean //mock 객체 만들어 빈으로 등록
EventRepository eventRepository;
/*...*/
Mockito.when(eventRepository.save(event)).thenReturn(event);
//이 코드들은 Mocking시 필요, SpringBootTest 사용하면 필요없어짐

2. (4) 입력값 제한하기

입력값 제한 : id 또는 입력 받은 데이터로 계산해야 하는 값들은 입력을 받지 않아야 한다.

EventDto 적용.

DTO -> 도메인 객체로 값 복사
ModelMapper를 dependency에 추가


통합 테스트로 전환
@WebMvcTest  => @SpringBootTest, @AutoConfigureMockMvc로 변경. Repository @MockBean 코드 제거.

2. (5) 입력값 이외에 에러 발생

ObjectMapper 커스터마이징

#application.properties에 작성
spring.jackson.deserialization.fail-on-unknown-properties=true

 

2. (6) Bad Request 처리하기

@Valid와 BindingResult (또는 Errors)
- BindingResult는 항상 @Valid 바로 다음 인자로 사용해야 함. (스프링 MVC)
- @NotNull, @NotEmpty, @Min, @Max, ... 사용해서 입력값 바인딩할 때 에러 확인할 수 있음

도메인 Validator 만들기 (EventValidator) - Validator 인터페이스 없이 만들어도 상관없음

테스트 설명용 애노테이션 만들기 - @Target, @Retention

2. (7) Bad Request 응답 본문 만들기

커스텀 JSON Serializer 만들기 -
- extends JsonSerializer<T> (Jackson JSON 제공)
- @JsonComponent (스프링 부트 제공)
-> ErrorsSerializer.java 클래스 생성


BindingError

  • FieldError 와 GlobalError (ObjectError)가 있음
  • objectName
  • defaultMessage
  • code
  • field
  • rejectedValue

2. (8) 비즈니스 로직 적용하기

<테스트할 것>

비즈니스 로직 적용 됐는지 응답 메시지 확인
- offline과 free 값 확인

2. (9) 매개변수를 이용한 테스트

테스트 코드 리팩토링

  • 테스트에서 중복 코드 제거
  • 매개변수만 바꿀 수 있으면 좋겠는데?
  • JUnitParams

JUnitParams dependency 추가함


실습 코드들

Event 클래스

package me.lsh.restapidemo.events;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Builder @AllArgsConstructor @NoArgsConstructor
@Getter @Setter @EqualsAndHashCode(of = "id")
@Entity
public class Event {

    @Id @GeneratedValue
    private Integer id;
    private String name;
    private String description;
    private LocalDateTime beginEnrollmentDateTime;
    private LocalDateTime closeEnrollmentDateTime;
    private LocalDateTime beginEventDateTime;
    private LocalDateTime endEventDateTime;
    private String location; // (optional) 이게 없으면 온라인 모임
    private int basePrice; // (optional)
    private int maxPrice; // (optional)
    private int limitOfEnrollment;
    private boolean offline;
    private boolean free;
    @Enumerated(EnumType.STRING)
    private EventStatus eventStatus;

    public void update() {
        if (this.basePrice == 0 && this.maxPrice == 0) {
            this.free = true;
        } else {
            this.free = false;
        }
        if (this.location == null || this.location.isBlank()) { //isBlank는 java 11에 추가됨.
            this.offline = false;
        } else {
            this.offline = true;
        }
    }
}

EventController 클래스

package me.lsh.restapidemo.events;

import jakarta.validation.Valid;
import org.modelmapper.ModelMapper;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.net.URI;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;

@Controller
@RequestMapping(value="/api/events", produces=MediaTypes.HAL_JSON_VALUE)
//이렇게 하면 methodOn 안써도됨
public class EventController {

    private final EventRepository eventRepository;

    private final ModelMapper modelMapper;

    private final EventValidator eventValidator;

    //생성자 주입
    public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator){
        this.eventRepository = eventRepository;
        this.modelMapper = modelMapper;
        this.eventValidator = eventValidator;
    }

    @PostMapping
    public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().build();
        }
        //body(event)는 된다(json으로 변환되어서 나감) 그러나 error는 못 담음
        //java BeanSerializer(serial: 객체를 json으로 변환) objectMapper 써서 변환
        //Java Bean Spec을 준수하지 않아서 변환할 수 없다 (HAR 따르지 x)

        eventValidator.validate(eventDto, errors);
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().build();
        }

        Event event = modelMapper.map(eventDto, Event.class);
        event.update(); //갱신. 원래는 Service쪽에 행위를 위임해도 됨
        Event newEvent = this.eventRepository.save(event);
        URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdUri).body(event);
    }
}

EventDto 클래스

package me.lsh.restapidemo.events;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data @Builder @AllArgsConstructor @NoArgsConstructor //Data 더 알아보기
public class EventDto {

    @NotEmpty //유효성검사
    private String name;
    @NotEmpty
    private String description;
    @NotNull
    private LocalDateTime beginEnrollmentDateTime;
    @NotNull
    private LocalDateTime closeEnrollmentDateTime;
    @NotNull
    private LocalDateTime beginEventDateTime;
    @NotNull
    private LocalDateTime endEventDateTime;
    private String location; // (optional) 이게 없으면 온라인 모임
    @Min(0)
    private int basePrice; // (optional)
    @Min(0)
    private int maxPrice; // (optional)
    @Min(0)
    private int limitOfEnrollment;

}

EventRepository 인터페이스

package me.lsh.restapidemo.events;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EventRepository extends JpaRepository<Event, Integer> {
}

EventStatus 클래스

package me.lsh.restapidemo.events;

public enum EventStatus {
    DRAFT, PUBLISHED, BEGAN_ENROLLMENT;
}

EventValidator 클래스

package me.lsh.restapidemo.events;

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;

import java.time.LocalDateTime;

@Component
public class EventValidator {

    public void validate(EventDto eventDto, Errors errors) { //validation의 Errors
        if (eventDto.getBasePrice() > eventDto.getMaxPrice() && eventDto.getMaxPrice() != 0) {
            errors.rejectValue("basePrice", "wrongValue", "BasePrice is Wrong.");
            errors.rejectValue("maxPrice", "wrongValue", "MaxPrice is Wrong.");
        }

        LocalDateTime endEventDateTime = eventDto.getEndEventDateTime();
        if (endEventDateTime.isBefore(eventDto.getBeginEventDateTime()) ||
        endEventDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) ||
        endEventDateTime.isBefore(eventDto.getBeginEnrollmentDateTime())) {
            errors.rejectValue("endEventDateTime", "wrongValue", "endEventDateTime is Wrong.");
        }

        //TODO beginEventDateTime
        //TODO CloseEnrollmentDateTime
    }
}

TestDescription 커스텀 애노테이션

package me.lsh.restapidemo.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TestDescription {
    String value();
}

ErrorsSerializer 클래스 (/common/)

package me.lsh.restapidemo.common;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.validation.Errors;

import java.io.IOException;

@JsonComponent //ObjectMapper는 Errors 객체를 Serialization할 때 사용한다.
public class ErrorsSerializer extends JsonSerializer<Errors> {

    @Override
    public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
        gen.writeStartArray();
        //여러개 에러 배열로 담아주기 위해
        //Validator에서 rejectValue(): 필드 에러, reject(): Global 에러
        errors.getFieldErrors().forEach(e -> { //.fE만 하니 그앞에 .stream()생략
            try {
                gen.writeStartObject();
                gen.writeStringField("field", e.getField());
                gen.writeStringField("objectName", e.getObjectName());
                gen.writeStringField("code", e.getCode());
                gen.writeStringField("DefaultMessage", e.getDefaultMessage());
                Object rejectedValue = e.getRejectedValue();
                if (rejectedValue != null) {
                    gen.writeStringField("rejectedValue", rejectedValue.toString());
                }
                gen.writeEndObject();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        });

        //Global
        errors.getGlobalErrors().forEach(e -> {
            try {
                gen.writeStartObject();
                gen.writeStringField("objectName", e.getObjectName());
                gen.writeStringField("code", e.getCode());
                gen.writeStringField("DefaultMessage", e.getDefaultMessage());
                gen.writeEndObject();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        });
        gen.writeEndArray();
    }
}

EventTest 클래스

package me.lsh.restapidemo.events;

import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(JUnitParamsRunner.class) //다운 필요
public class EventTest {

    @Test
    public void builder(){
        Event event = Event.builder()
                .name("Inflearn Spring REST API")
                .description("REST API development with Spring")
                .build();
        assertThat(event).isNotNull();
    }

    @Test
    public void javaBean() {
        // Given
        String name = "Event";
        String description = "Spring";

        // When
        Event event = new Event();
        event.setName(name);
        event.setDescription(description);

        // Then
        assertThat(event.getName()).isEqualTo(name);
        assertThat(event.getDescription()).isEqualTo(description);
    }

    @Test
    @Parameters //(method = "parametersForTestFree")
    /*@Parameters({
            "0, 0, true",
            "100, 0, false",
            "0, 100, false"
    })*/
    public void testFree(int basePrice, int maxPrice, boolean isFree) {
        Event event = Event.builder()
                .basePrice(basePrice)
                .maxPrice(maxPrice)
                .build();

        //When
        event.update();

        //Then
        assertThat(event.isFree()).isEqualTo(isFree);

        /* // 여기부분 코드 삭제 가능 (JUnitParamsRunner)
        //Given
        event = Event.builder()
                .basePrice(0)
                .maxPrice(100)
                .build();

        //When
        event.update();

        //Then
        assertThat(event.isFree()).isFalse();*/
    }

    private Object[] parametersForTestFree() {
        return new Object[] {
                new Object[] {0, 0, true},
                new Object[] {100, 0, false},
                new Object[] {0, 100, false},
                new Object[] {100, 100, false},
        };
    }

    @Test
    @Parameters
    public void testOffline(String location, boolean isOffline) {
        Event event = Event.builder()
                .location(location)
                .build();

        //When
        event.update();

        //Then
        assertThat(event.isOffline()).isEqualTo(isOffline);

        /*중복코드 삭제
        //Given
        event = Event.builder()
                .build();

        //When
        event.update();

        //Then
        assertThat(event.isOffline()).isFalse();*/
    }

    private Object[] parametersForTestOffline() {
        return new Object[] {
            new Object[] {"강남", true},
            new Object[] {null, false},
            new Object[] {" ", false},
        };
    }
}

EventControllerTests 클래스

package me.lsh.restapidemo.events;

import com.fasterxml.jackson.databind.ObjectMapper;
import me.lsh.restapidemo.common.TestDescription;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDateTime;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@RunWith(SpringRunner.class)
@SpringBootTest //Mvc용Mock
@AutoConfigureMockMvc
//@WebMvcTest //Mocking 안하자
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc; //가짜 요청 만들어서 보내고 응답 확인할 수 있는 Test 만들수있음.
    // Slicing Test 웹과 관련된 빈들만 테스트. dispatch sublet이란거 만들어야 함, 웹서버 띄우지x

    @Autowired
    ObjectMapper objectMapper;

    //@MockBean //목빈 //SpringBootTest 사용하면 필요없어짐
    //EventRepository eventRepository;

    @Test
    @TestDescription("정상적으로 이벤트를 생성하는 테스트")//JUnit5는 이런 기능 Annotation 지원
    public void createEvent() throws Exception {
        EventDto event = EventDto.builder()
                .name("Spring")
                .description("REST API Devlopment with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 8, 0))
                .closeEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 22, 0))
                .beginEventDateTime(LocalDateTime.of(2023, 11, 15, 19, 0))
                .endEventDateTime(LocalDateTime.of(2023, 11, 15, 21, 0))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("이대역")
                .build();
        //Mockito.when(eventRepository.save(event)).thenReturn(event);
        //Mocking을 안하겠습니다~
        mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaTypes.HAL_JSON) //확장자 처럼 만들음??
                        .content(objectMapper.writeValueAsString(event))) //안에다 주는게 요청, 하고나면 응답 나옴
                .andDo(print()) //어떤 요청, 어떤 응답 콘솔에서 볼수있
                .andExpect(status().isCreated())
                .andExpect(jsonPath("id").exists())
                .andExpect(header().exists(HttpHeaders.LOCATION))
                .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE))
                .andExpect(jsonPath("free").value(Matchers.not(false)))
                .andExpect(jsonPath("offline").value(Matchers.not(true))) //기본: false
                .andExpect(jsonPath("eventStatus").value(EventStatus.DRAFT))
        ;
    }


    @Test
    @TestDescription("입력받을 수 없는 값을 사용한 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request() throws Exception {
        Event event = Event.builder()
                .id(100)
                .name("Spring")
                .description("REST API Devlopment with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 8, 0))
                .closeEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 22, 0))
                .beginEventDateTime(LocalDateTime.of(2023, 11, 15, 19, 0))
                .endEventDateTime(LocalDateTime.of(2023, 11, 15, 21, 0))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("이대역")
                .free(true)
                .offline(false)
                .build();
        mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaTypes.HAL_JSON) //확장자 처럼 만들음??
                        .content(objectMapper.writeValueAsString(event))) //안에다 주는게 요청, 하고나면 응답 나옴
                        .andDo(print()) //어떤 요청, 어떤 응답 콘솔에서 볼수있
                        .andExpect(status().isBadRequest())
        ;
    }

    @Test
    @TestDescription("입력값이 비어있는 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Empty_Input() throws Exception {
        EventDto eventDto = EventDto.builder().build();

        this.mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(this.objectMapper.writeValueAsString((eventDto))))
                        .andExpect(status().isBadRequest());
    }

    @Test
    @TestDescription("입력값이 잘못된 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Wrong_Input() throws Exception {
        EventDto eventDto = EventDto.builder()
                .name("Spring")
                .description("REST API Devlopment with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 8, 0))
                .closeEnrollmentDateTime(LocalDateTime.of(2023, 11, 14, 22, 0))
                .beginEventDateTime(LocalDateTime.of(2023, 11, 15, 19, 0))
                .endEventDateTime(LocalDateTime.of(2023, 11, 15, 21, 0))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("이대역")
                .build();

        this.mockMvc.perform(post("/api/events")
                .contentType(MediaType.APPLICATION_JSON)
                .content(this.objectMapper.writeValueAsString((eventDto))))
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$[0].objectName").exists())
                .andExpect(jsonPath("$[0].field").exists())
                .andExpect(jsonPath("$[0].defaultMessage").exists())
                .andExpect(jsonPath("$[0].code").exists())
                .andExpect(jsonPath("$[0].rejectedValue").exists())
        ;

    }
}

오류 분투 기록..ㅎㅎ

EventTest 만들다가 똑같이 따라했는데 오류 발생

before

커서 부분에 슬래시를 지워주니 해결되었다...

after~

이건 @Id 애노테이션 임포트를 잘못했다; 어쩐지.. @Id 붙였는데 없다고 뜨더라...

그냥 지워주면 해결된다. 위에 import jakarta.persistence.*에서 import되어있다.

잘못된 import

@Test 애노테이션 임포트

JUnit4의 Test를 사용하고 있으므로 아래 경로로 임포트한다

import org.junit.Test;

Errors의 임포트도 validation에 있는거로 잘 보고 해야한다.

import org.springframework.validation.Errors;

 

testFree 메소드 코드를 작성하던 중 InitializationException이 발생했는데, 오류가 발생했던 이전의 잘못된 파일을 Run 하고 있어서 그런거였다...

당시 오류 메세지: java.lang.Exception: No tests found matching Method testFree

참고한 글: https://stackoverflow.com/questions/40863593/java-lang-exception-no-tests-found-matching-method-using-intellij-idea

 

그리고 Problem에 Lombok builder is missing non nullable fields 뜨는데 빨간색인데 있어도 Run이 잘 되긴 한다. 아직 적합한 초기화 과정을 진행하지 않아서 그런듯하므로 강의를 진행하는 과정중에 해결될 것 같다.