포스트

REST API(12) Event 조회 및 수정 REST API 개발

구현 Package 및 Class

image


Event 목록 조회 API 구현

Event 목록을 조회하는 API를 구현해 보겠다.
테스트 해야할 것들은 다음과 같다.

  • 이벤트를 30개를 만들고, 10개 사이즈로 두번째 페이지 조회, pageable 확인
  • Page 링크 정보 추가
  • 각각의 Event로 가는 링크(self) 및 profile 링크 추가
  • query-events 문서화

이벤트를 30개를 만들고, 10개 사이즈로 두번째 페이지 조회, pageable 확인

이벤트를 30개를 만들고, 이벤트 목록 출력시에 한 페이지에 10개씩 해서, 두 번째 페이지를 보여주도록 구현해보자.

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// import 생략
import java.util.stream.IntStream; // 추가

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; // 추가

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository; // 추가    

    // 이 외 테스트 코드 생략

    @Test
    @TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
    public void queryEvents() throws Exception{
        // Given
        IntStream.range(0, 30).forEach(this::genertateEvent);

        // When
        this.mockMvc.perform(get("/api/events")
                        .param("page","1")
                        .param("size","10")
                        .param("sort","name,DESC")
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("pageable").exists())
        ;
    }

    private void genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .build();

        this.eventRepository.save(event);
    }
}

  • IntStream.range(0, 30).forEach(this::genertateEvent);를 통해 이벤트를 30개 만들어 준다.
    • "name" : "event 1~30"
    • "decription" : "test event"
  • MockMVC를 통해 "/api/events" 경로로 Get 요청을 보낸다.
    • param() 메서드를 통해 파라미터 값을 보낸다.
  • 페이징과 정렬은 스프링 데이터 JPA가 제공하는 Pageable 를 이용해서 구현할 수 있다.
    • page : 1은 두 번째 페이지 (0부터 시작)
    • size : 한 페이지에 10개씩 (기본값 20)
    • sort : ("name","DESC") 👉 이름을 내림차순으로 정렬 (property,property)

EventController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// import 생략
import org.springframework.data.domain.Pageable; // 추가


@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
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;
    }
    // 이 외 코드 생략

    @GetMapping // 추가
    public ResponseEntity queryEvents(Pageable pageable) { 
        return ResponseEntity.ok(this.eventRepository.findAll(pageable));
    }
}


결과(성공)

image
image

  • 이벤트가 총 30개 생성 되고, pageNumber는 1(두번째 페이지), pageSize는 10으로 pageable된 것을 확인 할 수 있다.

Page 링크 정보 추가

page를 Resource로 바꿔서 링크 정보를 추가해야하는데, 이 때 SpringData JPA가 제공해주는 PagedResourceAssembler가 있다.

EventController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// import 생략 
import org.springframework.data.web.PagedResourcesAssembler;
//import org.springframework.hateoas.PagedResources;
//import org.springframework.hateoas.Resource;  


@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
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;
    }
    // 이 외 코드 생략

    @GetMapping
    public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) { // 수정
        Page<Event> page = this.eventRepository.findAll(pageable); // 수정
        var pagedResources = assembler.toResource(page); // 수정
        //PagedResources<Resource<Event>> pagedResources = assembler.toResource(page);
        return ResponseEntity.ok(pagedResources);
    }
}
  • SpringData JPA가 제공해주는 PagedResourceAssembler를 통해 pageResource화 해준다.
  • Java에서 제공하는 var타입으로 긴 코드를 간소화 할 수 있다.

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;    

    // 이 외 테스트 코드 생략

    @Test
    @TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
    public void queryEvents() throws Exception{
        // Given
        IntStream.range(0, 30).forEach(this::genertateEvent);

        // When
        this.mockMvc.perform(get("/api/events")
                        .param("page","1")
                        .param("size","10")
                        .param("sort","name,DESC")
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("page").exists()) // 수정
        ;
    }

    private void genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .build();

        this.eventRepository.save(event);
    }
}


결과 (성공)

image
image

  • "_embedded"안에 "eventList"들어간 것을 확인할 수 있고
  • "page" 페이지 정보와 "_links" 내에 이전/다음("prev","next"), 처음("first"), 마지막("last") 페이지로 가는 링크들도 확인해 볼 수 있다.

각각의 Event로 가는 링크(self) 및 profile 링크 추가, query-events 문서화

이벤트 목록 페이지 자체의 self링크는 있지만, 각각의 이벤트 자신의 self링크는 없다. 이를 추가해 주자.

EventController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
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;
    }
    // 이 외 코드 생략

    @GetMapping
    public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) { 
        Page<Event> page = this.eventRepository.findAll(pageable);
        var pagedResources = assembler.toResource(page, e -> new EventResource(e)); // 수정
        pagedResources.add(new Link("/docs/index.html#resources-events-list").withRel("profile")); // 추가
        return ResponseEntity.ok(pagedResources);
    }
}
  • 람다 표현식으로 간단하게 각각의 이벤트에 대한 self링크를 추가해 줄 수 있다.
  • profile로 가는 링크를 추가해준다.

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    // 이 외 테스트 코드 생략

    @Test
    @TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
    public void queryEvents() throws Exception{
        // Given
        IntStream.range(0, 30).forEach(this::genertateEvent);

        // When
        this.mockMvc.perform(get("/api/events")
                        .param("page","1")
                        .param("size","10")
                        .param("sort","name,DESC")
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("page").exists())
                .andExpect(jsonPath("_embedded.eventList[0]._links.self").exists())
                .andExpect(jsonPath("_links.self").exists())
                .andExpect(jsonPath("_links.profile").exists())
                .andDo(document("query-events")
                    // 문서화 생략
                )
        ;
    }

    private void genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .build();

        this.eventRepository.save(event);
    }
}

  • .andDo(document("query-events"))로 문서화 시에 이벤트 생성 API (createEvent())처럼 문서화 해줘야하지만, 그 과정은 생략하겠다.

결과 (성공)

image
image

Event 조회 API 구현

이번에는 이벤트 한 건에 대한 데이터를 조회하는 API를 구현해 보겠다.
테스트 해야할 것들은 다음과 같다.

  • 존재하는 이벤트 리소스 확인
  • 존재하지 않는 이벤트 404응답 확인
  • get-an-event 문서화

존재하는 이벤트 리소스 확인 , 존재하지 않는 이벤트 404응답 확인

이벤트를 조회할 때 존재하는 이벤트에 대한 리소스(데이터+링크)를 확인할 수 있어야 하고,
존재하지 않는 이벤트를 조회할 때는 Not Found(404) 응답을 확인 할 수 있어야 한다.

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    // 이 외 테스트 코드 생략

    private Event genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .build();

        return this.eventRepository.save(event);
    }

    @Test  // [1] 
    @TestDescription("기존의 이벤트를 하나 조회하기")
    public void getEvent() throws Exception{
        // Given
        Event event = this.genertateEvent(100);

        // When & Then
        this.mockMvc.perform(get("/api/events/{id}", event.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("name").exists())
                .andExpect(jsonPath("id").exists())
                .andExpect(jsonPath("_links.self").exists())
                .andExpect(jsonPath("_links.profile").exists())
                .andDo(document("get-an-event"))
        ;
    }

    @Test // [2]
    @TestDescription("없는 이벤트를 조회했을 때 404 응답받기")
    public void getEvent404() throws Exception{
        // When & Then
        this.mockMvc.perform(get("/api/events/11883"))
                .andExpect(status().isNotFound());
    }
}
  • [1] : 존재하는 이벤트 하나를 조회하는 테스트이다.
    • 먼저 이벤트(100)를 하나 생성한다.
    • "/api/events/100" 경로로 Get 요청을 보낸다.
    • 응답 상태를 확인하고, name,id 데이터와 self,profile링크 정보가 있는지 확인한다.
    • "get-an-event"로 문서화 한다.
  • [2] : 존재하지 않는 이벤트를 조회하여 Not Found(404)응답을 얻는 테스트이다.
    • 이벤트를 생성하는 과정을 생략한다.
    • "/api/events/11883"라는 존재하지 않는 아이디(11883)에 대한 Get 요청을 보낸다.
    • Not Found(404)응답을 확인한다.

EventController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
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;
    }
    // 이 외 코드 생략

    @GetMapping("/{id}")
    public ResponseEntity getEvent(@PathVariable Integer id){
        Optional<Event> optionalEvent = this.eventRepository.findById(id);
        if (optionalEvent.isEmpty()) {
            return ResponseEntity.notFound().build();
        }

        Event event = optionalEvent.get();
        EventResource eventResource = new EventResource(event);
        eventResource.add(new Link("/docs/index.html#resources-events-get").withRel("profile"));
        return ResponseEntity.ok(eventResource);
    }
}
  • @PathVariable
    • 경로 변수를 표시하기 위해 메서드의 매개변수에 사용된다.
    • 경로 변수는 중괄호 {}로 둘러싸인 값을 나타낸다.
  • Optional타입으로 this.eventRepository.findById(id); JPA 메서드를 통해 DB에서 id에 대한 데이터를 매핑하여 찾는다.
    • Optional클래스는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로 NPE(NullPointerException)을 방지한다.
  • 객체 값이 비어있으면 Not Found(404)응답 리턴
  • 값이 있으면 event 객체에 get()을 통해 담아주고
    • event객체를 EventResource타입으로 리소스화 해준다.
    • 이벤트에 해당하는 profile링크를 eventResource에 추가해준다.
  • eventResource 객체를 리턴한다.

결과 (성공)

image


Event 수정 API 구현

만든 이벤트를 수정하는 API를 구현해 보자.
테스트 해야할 것들은 다음과 같다.

  • 정상적으로 수정한 경우 이벤트 리소스 응답
  • 입력 값이 잘못되어 있는 경우 이벤트 수정 실패
    • 도메인 로직으로 데이터 검증 실패시, Bad Request(400)응답 확인
    • 입력 데이터(바인딩하는 데이터)가 이상한 경우 Bad Request(400)응답 확인
  • 수정하려는 이벤트가 없는 경우 Not Found(404)응답 확인

정상적으로 수정한 경우 이벤트 리소스 응답, 문서화

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    @Autowired
    ModelMapper modelMapper; //추가

    // 이 외 테스트 코드 생략

    private Event genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
                .beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
                .endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타트업 팩토리")
                .free(false)
                .offline(true)
                .eventStatus(EventStatus.DRAFT)
                .build();

        return this.eventRepository.save(event);
    }

    @Test
    @TestDescription("이벤트를 정상적으로 수정하기")
    public void updateEvent() throws Exception{
        // Given
        Event event = this.genertateEvent(200);

        EventDto eventDto = this.modelMapper.map(event, EventDto.class);
        String eventName = "Update Event";
        eventDto.setName(eventName);

        // When & Then
        this.mockMvc.perform(put("/api/events/{id}", event.getId())
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(this.objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("name").value(eventName))
                .andExpect(jsonPath("_links.self").exists())
                .andDo(document("update-event"))
        ;
    }
}
  • 이벤트(200)을 생성해준다.
  • ModelMapper를 이용하여 event 객체를 eventDto객체에 덮어준다.
  • name을 수정("Update Event"으로) 해준다.
  • "/api/events/{id}"경로로 Put해준다.
    • ObjectMapper를 통해 eventDto 객체를 JSON 값으로 보낸다.
  • name"Update Event"로 수정되었는지 확인한다.
  • self,profile 링크가 있는지 확인한다.
  • "update-event"로 문서화 한다.

도메인 로직으로 데이터 검증 실패시, Bad Request(400)응답 확인

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    @Autowired
    ModelMapper modelMapper; //추가

    // 이 외 테스트 코드 생략

    private Event genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
                .beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
                .endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타트업 팩토리")
                .free(false)
                .offline(true)
                .eventStatus(EventStatus.DRAFT)
                .build();

        return this.eventRepository.save(event);
    }

    @Test
    @TestDescription("입력값이 비어있는 경우에 이벤트 수정 실패")
    public void updateEvent400_Empty() throws Exception{
        // Given
        Event event = this.genertateEvent(200);

        EventDto eventDto = new EventDto();

        // When & Then
        this.mockMvc.perform(put("/api/events/{id}", event.getId())
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(this.objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}
  • 이벤트(200)을 생성해준다.
  • eventDto 객체를 생성하되, 값을 넣어주지 않는다.
  • eventDto 객체를 보내주고 Bad Request(400)응답을 확인한다.

입력 데이터(바인딩하는 데이터)가 이상한 경우 Bad Request(400)응답 확인

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    @Autowired
    ModelMapper modelMapper; //추가

    // 이 외 테스트 코드 생략

    private Event genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
                .beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
                .endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타트업 팩토리")
                .free(false)
                .offline(true)
                .eventStatus(EventStatus.DRAFT)
                .build();

        return this.eventRepository.save(event);
    }

    @Test
    @TestDescription("입력값이 잘못된 경우에 이벤트 수정 실패")
    public void updateEvent400_Wrong() throws Exception{
        // Given
        Event event = this.genertateEvent(200);

        EventDto eventDto = this.modelMapper.map(event, EventDto.class);
        eventDto.setBasePrice(20000);
        eventDto.setMaxPrice(1000);

        // When & Then
        this.mockMvc.perform(put("/api/events/{id}", event.getId())
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(this.objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}
  • 이벤트(200)을 생성해준다.
  • event 객체 값을 eventDto 덮어주고, basePrice값과 maxPrice값을 비즈니스 로직상 잘못된 값으로 수정한다.
  • eventDto 객체를 보내주고 Bad Request(400)응답을 확인한다.

수정하려는 이벤트가 없는 경우 Not Found(404)응답 확인

EventControllerTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    EventRepository eventRepository;  

    @Autowired
    ModelMapper modelMapper; //추가

    // 이 외 테스트 코드 생략

    private Event genertateEvent(int index) {
        Event event = Event.builder()
                .name("event " + index)
                .description("test event")
                .beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
                .beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
                .endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타트업 팩토리")
                .free(false)
                .offline(true)
                .eventStatus(EventStatus.DRAFT)
                .build();

        return this.eventRepository.save(event);
    }

    @Test
    @TestDescription("존재하지 않는 이벤트 수정 실패")
    public void updateEvent404() throws Exception{
        // Given
        Event event = this.genertateEvent(200);

        EventDto eventDto = this.modelMapper.map(event, EventDto.class);

        // When & Then
        this.mockMvc.perform(put("/api/events/1231231")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(this.objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                .andExpect(status().isNotFound());
    }
}
  • 이벤트(200)을 생성해준다.
  • "/api/events/1231231"라는 존재하지 않는 id값으로 Put요청을 보낸다.
  • Not Found(404)응답을 확인한다.

EventController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
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;
    }
    // 이 외 코드 생략 

    private  ResponseEntity badRequest(Errors errors){
        return ResponseEntity.badRequest().body(new ErrorsResource(errors));
    }

    @PutMapping("/{id}")
    public ResponseEntity updateEvent(@PathVariable Integer id,
                                      @RequestBody @Valid EventDto eventDto,
                                      Errors errors) {
        Optional<Event> optionalEvent = this.eventRepository.findById(id);
        if (optionalEvent.isEmpty()){
            return ResponseEntity.notFound().build();
        }
        // 도메인 로직 검증
        if(errors.hasErrors()){
            return  badRequest(errors);
        }
        // 비즈니스 로직 검증
        this.eventValidator.validate(eventDto, errors);
        if (errors.hasErrors()){
            return badRequest(errors);
        }

        Event existingEvent = optionalEvent.get();
        //existingEvent.setName(eventDto.getName());
        this.modelMapper.map(eventDto, existingEvent);
        Event savedEvent = this.eventRepository.save(existingEvent);
        EventResource eventResource = new EventResource(savedEvent);
        eventResource.add(new Link("/docs/index.html#resources-events-update").withRel("profile"));

        return ResponseEntity.ok(eventResource);
    }
}
  • @PathVariable로 경로 변수 id값을 받아온다.
  • @RequestBody로 요청 본문에 EventDto타입의 eventDto객체를 받는다.
  • @ValidEventDto의 도메인 로직을 검증하고, 발생한 에러를 errors 객체에 담는다.
  • DB에서 id에 대한 데이터를 매핑하여 찾아 optionalEvent 객체에 담는다.
  • optionalEvent 객체 값이 비어있으면 Not Found(404)응답 리턴.
  • @Valid로 도메인 로직으로 데이터를 검증하여 발생한 에러가 있을 시, Bad Request(400)응답 리턴.
    (이때, ErrorResource도 리턴하여, index링크도 출력)
  • EventValidator로 비즈니스 로직으로 데이터를 검증하여 발생한 에러가 있을 시, Bad Request(400)응답 리턴.
    (위와 같음)
  • optionalEvent이 있으면 existingEvent객체에 담아준다.
  • ModelMapper로 수정하고 싶은 값(eventDto객체에 담긴 값)을 existingEvent객체의 값에 덮어준다.
  • eventRepository에 저장하고 savedEvent객체에 담아준다.
    • 현재 어떠한 트랜잭션에 들어있는것이 아니기 때문에, 변경사항이 자동으로 dirty checking되어 데이터베이스에 반영되지 않기 때문에 명시적으로 save하는 과정이 꼭 필요하다.
    • Service를 만들어야 데이터베이스에 반영된다.
  • savedEventeventResource로 리소스화 하고, profile링크를 추가해 준다.
  • eventResource를 응답 body에 담아 리턴

결과 (성공)

image


테스트 코드 리팩토링 (추가)

1
2
3
4
5
6
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
1
2
3
4
5
6
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;
    @Autowired
    ModelMapper modelMapper;

EventControllerTestsIndexControllerTest 코드를 보면 위와 같이 중복으로 사용하는 코드가 많다.
그리고 그 코드들은 앞으로의 컨트롤러 테스트 코드 작성시에도 똑같이 중복으로 사용될 것이다.
BaseControllerTest라는 클래스를 만들어 중복되는 코드를 모아주고, 이를 상속받아서 사용하는 식으로 리팩토링하여 코드를 간소화 해보자.

BaseControllerTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package me.hantomas.restapi.common;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
@Ignore
public class BaseControllerTest {
    @Autowired
    protected MockMvc mockMvc;
    @Autowired
    protected ObjectMapper objectMapper;
    @Autowired
    protected ModelMapper modelMapper;
}

  • @Ignore : Junit이 제공하는 어노테이션으로, 테스트를 가지고있는 클래스로 간주되지 않도록 한다.

EventControllerTests & IndexControllerTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package me.hantomas.restapi.events;

import me.hantomas.restapi.common.BaseControllerTest;
import me.hantomas.restapi.common.TestDescription;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import java.time.LocalDateTime;
import java.util.stream.IntStream;

import static org.springframework.restdocs.headers.HeaderDocumentation.*;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


public class EventControllerTests extends BaseControllerTest {

    @Autowired
    EventRepository eventRepository;

    // 테스트 코드 생략
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package me.hantomas.restapi.index;

import me.hantomas.restapi.common.BaseControllerTest;
import org.junit.Test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;



public class IndexControllerTest extends BaseControllerTest {

    @Test
    public void index() throws Exception {
        this.mockMvc.perform(get("/api/"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("_links.events").exists());
    }
}

  • import문이 눈에 띄게 줄어든것만 봐도 코드가 간소화 된것을 알 수 있다.

결과 (성공)

image

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.