포스트

REST API(4) Event 생성 API 구현 - Event Repository 구현

구현 Package 및 Class

image

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> {
}
  • EventRepositoryinterface로 만들어 주고, 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 키워드를 통한 불변, 문제 발생시 컴파일 타임에서의 에러 등등 여러 장점들이 있어서, 공식 문서 및 실무에서도 생성자 주입 방식을 선호하고 사용해야 한다고 한다.

결과(오류)

image

  • @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)

image

  • 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.
  • 추가로 HeaderLocation이 있는지 Content-Type은 요청한대로 나오는지 확인 가능하다.

결과 (성공)

image

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