구현 Package 및 Class
Event Repository
event
객체를 실제 DB에 저장할 수 있도록 EventRepository
를 구현해 보자.
Event
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
| package me.hantomas.restapi.events;
import lombok.*;
import javax.persistence.*;
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;
private int basePrice;
private int maxPrice;
private int limitOfEnrollment;
private boolean offline;
private boolean free;
@Enumerated(EnumType.STRING)
private EventStatus eventStatus = EventStatus.DRAFT;
}
|
@Entity
: 우선 Event
를 Entity를 만들어 주기위해 @Entity
어노테이션을 선언한다.@Id
: JPA로 테이블과 엔티티를 매핑할 때, 식별자로 사용할 필드 위에 @Id
어노테이션을 붙여 테이블의 Primary Key와 연결 시켜줘야한다.@GeneratedValue
: 이 어노테이션을 통해 식별자 값을 자동으로 생성한다.@Enumerated(EnumType.STRING)
: 기본 EnumType은 ORDINAL
인데, 이는 enum
의 순서에 따라 0번부터 정수를 반환한다. 따라서 STRING
타입을 바꾸어, 저장된 문자열로 반환하도록 하고, 나중에 enum
의 순서가 바뀌어도 데이터가 꼬이는 것을 방지할 수 있다.
EventRepository
1
2
3
4
5
6
| package me.hantomas.restapi.events;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EventRepository extends JpaRepository<Event, Integer> {
}
|
EventRepository
를 interface
로 만들어 주고, JpaRepository<T, id>
를 상속 받는다.JpaRepository
를 사용하면, 복잡한 JDBC(Java DataBase Connectivity) 코드를 작성하지 않아도 간단하게 DB와의 데이터 접근 작업을 처리할 수 있다.JPARepository 인터페이스는 제네릭 타입을 사용하여 Entity클래스와 해당 Entity의 ID클래스를 명시한다.
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
| package me.hantomas.restapi.events;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.mvc.ControllerLinkBuilder.linkTo;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
public EventController(EventRepository eventRepository){
this.eventRepository = eventRepository;
}
@PostMapping
public ResponseEntity createEvent(@RequestBody Event event){
Event newEvent = this.eventRepository.save(event);
URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri(); // "{id}"를 newEvent.getId()로 수정
return ResponseEntity.created(createdUri).body(newEvent); // 수정
}
}
|
1
2
3
4
5
| private final EventRepository eventRepository;
public EventController(EventRepository eventRepository){
this.eventRepository = eventRepository;
}
|
- 위에서 EventRepository를
@AutoWired
를 사용하지 않고, 이 처럼 생성자 주입 방식으로 주입하였다. - 클래스가 생성될 당시 초기 실행과 한 번 실행의 보장, final 키워드를 통한 불변, 문제 발생시 컴파일 타임에서의 에러 등등 여러 장점들이 있어서, 공식 문서 및 실무에서도 생성자 주입 방식을 선호하고 사용해야 한다고 한다.
결과(오류)
@WebMvcTest
은 슬라이스 테스트로서, 웹용 빈들만 등록을 해주고,Repository
를 빈으로 등록해주지 않아서 그런다고 한다.
오류 수정 (EvnetControllerTests)
따라서, Repository
를 Mocking해주면 된다.
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
| package me.hantomas.restapi.events;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
EventRepository eventRepository; // 추가
@Test
public void createEvent() throws Exception{
Event event = Event.builder()
.name("Spring")
.description("REST API Development With Spring")
.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 스타트업 팩토리")
.build();
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(event))
)
.andDo(print())
.andExpect(status().isCreated())
.andExpect(jsonPath("id").exists())
;
}
}
|
결과(오류) (2)
- NullPointException :
EventControllerTests
에서 Mocking한 Repository
는 Mock 객체이기 때문에 EventController
에서 save()
해도 리턴 되는 값은 Null
이다.
오류 수정 (EvnetControllerTests)
따라서, EventController
에서 save()
가 호출 됐을 때, EventControllerTests에서 어떤 식으로 동작을 해야하는 지 Stubbing1 해주면 된다.
1: 테스트 스텁(Test Stub)은 테스트 호출 중 테스트 스텁은 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 것
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
| package me.hantomas.restapi.events;
import com.fasterxml.jackson.databind.ObjectMapper;
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.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
EventRepository eventRepository;
@Test
public void createEvent() throws Exception{
Event event = Event.builder()
.name("Spring")
.description("REST API Development With Spring")
.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 스타트업 팩토리")
.build();
event.setId(10); // 추가
Mockito.when(eventRepository.save(event)).thenReturn(event); // 추가
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.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_UTF8_VALUE)) //추가
;
}
}
|
event.setId(10);
: event에 id를 10으로 추가해 주고.Mockito.when(eventRepository.save(event)).thenReturn(event);
: eventRepository.save(event)
가 호출 되었을 때, event
를 리턴하라고 Stubbing.- 추가로
Header
에 Location
이 있는지 Content-Type
은 요청한대로 나오는지 확인 가능하다.
결과 (성공)