구현 Package 및 Class
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));
}
}
|
결과(성공)
- 이벤트가 총 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를 통해 page를 Resource화 해준다. - 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);
}
}
|
결과 (성공)
"_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())처럼 문서화 해줘야하지만, 그 과정은 생략하겠다.
결과 (성공)
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 객체를 리턴한다.
결과 (성공)
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객체를 받는다.@Valid로 EventDto의 도메인 로직을 검증하고, 발생한 에러를 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를 만들어야 데이터베이스에 반영된다.
savedEvent를 eventResource로 리소스화 하고, profile링크를 추가해 준다.eventResource를 응답 body에 담아 리턴
결과 (성공)
테스트 코드 리팩토링 (추가)
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;
|
EventControllerTests와 IndexControllerTest 코드를 보면 위와 같이 중복으로 사용하는 코드가 많다.
그리고 그 코드들은 앞으로의 컨트롤러 테스트 코드 작성시에도 똑같이 중복으로 사용될 것이다.
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문이 눈에 띄게 줄어든것만 봐도 코드가 간소화 된것을 알 수 있다.
결과 (성공)