티스토리 뷰

반응형

MockMVC란?

MockMvc는 Controller테스트 할때 사용합니다. 왜냐하면 Controller에서 컨트롤러의 요청과 응답에 대한 테스트를 해야하는데 MockMvc가 실제 HTTP 요청을 보내지 않고 Application내부에서 요청/응답 처리를 해주기 때문입니다.

 

*초기화 방법

1)

@AutoConfigureMockMvc
@SpringBootTest
class FoodControllerTest {

private MockMvc mockMvc;
    ...
mockMvc = MockMvcBuilders.standaloneSetup(new FoodController(foodService)).build();

2)

@AutoConfigureMockMvc
@SpringBootTest
class FoodControllerTest {

@Autowired
private MockMvc mockMvc;

*ObjectMapper란?

또한 MockMvc가 HTTP 요청을 보낸 결과로 반환된 JSON 문자열을 String으로 변환하기 위해 ObjectMapper클래스를 사용합니다.

 

 

에러) 두개의 GetMapping이 같은 주소를 쓸때 충돌

상황

@GetMapping("/{id}")
public FoodResponseDto searchFoodById(@PathVariable Long id) {
    return foodService.findById(id);
}


@GetMapping("/{id}")
public List<FoodResponseDto> searchFoodContainName(@PathParam("name") String name) {
    return foodService.findByNameContaining(name);
}

 

에러

Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous handler methods mapped for '/foods/1': {public com.programmers.dto.FoodResponseDto com.programmers.controller.FoodController.searchFoodById(java.lang.Long), public java.util.List com.programmers.controller.FoodController.searchFoodContainName(java.lang.String)} org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous handler methods mapped for '/foods/1': {public com.programmers.dto.FoodResponseDto com.programmers.controller.FoodController.searchFoodById(java.lang.Long), public java.util.List com.programmers.controller.FoodController.searchFoodContainName(java.lang.String)}

 

이유

이 오류는 "/foods/1" 엔드포인트에 대해 여러 개의 핸들러 메서드가 매핑되어 있다는 것을 의미

즉, "searchFoodById(Long id)"와 "searchFoodContainName(String name)" 두 메서드가 모두 "/foods/1"을 처리할 수 있음. 

이 문제를 해결하기 위해서는

1.@RequestMapping 애너테이션에 method 속성을 추가하여 HTTP 요청 메서드를 구체화

2.@GetMapping 애너테이션을 사용하여 GET 요청만을 처리하도록 지정

 

해결

@GetMapping("/{id}")
public FoodResponseDto searchFoodById(@PathVariable Long id) {
    return foodService.findById(id);
}


@GetMapping("/search/{name}")
public List<FoodResponseDto> searchFoodContainName(@PathParam("name") String name) {
    return foodService.findByNameContaining(name);
}

 

 

에러) Json의 Body가 Null인 에러1

상황

@RequiredArgsConstructor
@RequestMapping("/foods")
public class FoodController {
   private final FoodService foodService;
            ...
}

 

에러

No value at JSON path "$.id"
java.lang.AssertionError: No value at JSON path "$.id"
at org.springframework.test.util.JsonPathExpectationsHelper.evaluateJsonPath(JsonPathExpectationsHelper.java:304)
at org.springframework.test.util.JsonPathExpectationsHelper.assertValue(JsonPathExpectationsHelper.java:99)
at org.springframework.test.web.servlet.result.JsonPathResultMatchers.lambda$value$2(JsonPathResultMatchers.java:111)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at com.programmers.controller.FoodControllerTest.searchFoodById(FoodControllerTest.java:79)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

 

해결방법

@RequiredArgsConstructor
@RequestMapping("/foods")
@RestController
public class FoodController {
   private final FoodService foodService;
}

 

 

@RestController와 @Controller차이

@Controller는 View를 반환하기 위해 사용됩니다.

View를 반환하기 위해서는 메소드가 반환하는 값이 View 이름이거나 ModelAndView 객체여야 합니다. 주로 화면을 보여주는 웹 어플리케이션에서 사용됩니다.

 @RestController는 JSON 형태의 데이터를 반환하기 위해 사용됩니다.

반환된 데이터는 HTTP 응답 본문에 포함되어 전달됩니다. 주로 RESTful 웹 서비스를 개발할 때 사용됩니다.

 

즉, @Controller는 View를 반환하는 것에 중점을 두고, @RestController는 JSON 형태의 데이터를 반환하는 것에 중점을 둡니다. 따라서, @RestController를 사용하면 컨트롤러에서 직접 JSON 데이터를 반환할 수 있으므로, 추가적인 View 템플릿이 필요하지 않습니다.

 

에러) Get메소드사용시 Body가 "<no character encoding set>"인 이유(=contentType(MediaType.APPLICATION_JSON)을 지정할 필요가 없는 이유)

코드

mockMvc.perform(get("/foods/search")
                .param("name", name)
                .contentType(MediaType.APPLICATION_JSON))

 

 

로그

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /foods/1
       Parameters = {}
          Headers = [Content-Type:"application/json"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.programmers.controller.FoodController
           Method = com.programmers.controller.FoodController#searchFoodById(Long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"name":"lamen","category":"noodle","price":1000,"description":"맛있는라면","image":null}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

이유

MockMvc의 perform() 메소드에서 GET 메소드와 함께 호출한 경우, 요청 파라미터는 Parameters 속성으로 나타납니다. 따라서 출력 결과에서 요청 URI의 파라미터인 name=lamen이 Parameters 속성으로 표시되고 있습니다.

하지만 GET 메소드는 요청 본문(body)을 가질 수 없기 때문에 Body 속성에는 "<no character encoding set>"이 출력됩니다.contentType(MediaType.APPLICATION_JSON)을 지정할 필요가 없습니다.

따라서 contentType(MediaType.APPLICATION_JSON)를 제거합니다.

 

 

에러) Json의 Body가 Null인 에러2

상황

@Test
void searchFoodContainName() throws Exception {
    //given
    String name = "ramen";
    List<FoodResponseDto> expectedFoods = new ArrayList<>();
    expectedFoods.add(FoodResponseDto.of(basicFoodData()));
    expectedFoods.add(FoodResponseDto.of(dummyFoodData()));

    //when,then
    mockMvc.perform(get("/foods/search")
                    .param("name", name)
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.[0].id").value(String.valueOf(expectedFoods.get(0).getId())))
            .andExpect(jsonPath("$.[0].name").value(expectedFoods.get(0).getName()))
            .andExpect(jsonPath("$.[0].price").value(expectedFoods.get(0).getPrice()))
            .andExpect(jsonPath("$.[0].description").value(expectedFoods.get(0).getDescription()))
            .andExpect(jsonPath("$.[0].category").value(expectedFoods.get(0).getCategory()));
}

에러

No value at JSON path "$.[0].id" java.lang.AssertionError: No value at JSON path "$.[0].id" at org.springframework.test.util.JsonPathExpectationsHelper.evaluateJsonPath(JsonPathExpectationsHelper.java:304) at org.springframework.test.util.JsonPathExpectationsHelper.assertValue(JsonPathExpectationsHelper.java:99) at org.springframework.test.web.servlet.result.JsonPathResultMatchers.lambda$value$2(JsonPathResultMatchers.java:111) at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214) at com.programmers.controller.FoodControllerTest.searchFoodContainName(FoodControllerTest.java:103) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:567)

 

해결방법

 @Test
    void searchFoodContainName() throws Exception {
        //given
        String name = "lamen";
        List<FoodResponseDto> expectedFoods = new ArrayList<>();
        expectedFoods.add(FoodResponseDto.of(basicFoodData()));
        //when,then
        mockMvc.perform(get("/foods/search?name={name}", name))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.id").value(String.valueOf(expectedFoods.get(0).getId())))
                .andExpect(jsonPath("$[0].name").value(expectedFoods.get(0).getName()))
                .andExpect(jsonPath("$[0].price").value(expectedFoods.get(0).getPrice()))
                .andExpect(jsonPath("$[0].description").value(expectedFoods.get(0).getDescription()))
                .andExpect(jsonPath("$[0].category").value(expectedFoods.get(0).getCategory()));
    }

 

틀린부분

해당 API는 GET 요청으로 name 파라미터를 받아와서 해당 이름을 포함한 음식을 조회하는 기능을 수행한다. 따라서 @RequestParam을 사용하여 name 파라미터를 받아와야 한다.

"/foods/search?name={name}"

 

 

에러 Id가 null인 이유

상황 

@Getter
@NoArgsConstructor
@Entity
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
 }

 

에러

2023-03-26 03:48:36.359  WARN 14796 --- [    Test worker] o.m.jdbc.message.server.ErrorPacket      : Error: 1364-HY000: Field 'id' doesn't have a default value
2023-03-26 03:48:36.361  WARN 14796 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1364, SQLState: HY000
2023-03-26 03:48:36.361 ERROR 14796 --- [    Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper   : (conn=155) Field 'id' doesn't have a default value

 

 

해결방법

@GeneratedValue 어노테이션은 엔티티의 기본 키를 자동으로 생성하기 위해 사용되는 어노테이션 중 하나입니다. 이 어노테이션이 지정되지 않은 경우, 엔티티의 기본 키 값을 직접 설정해야 합니다.

테스트 코드에서 basicFoodData() 메소드를 호출하여 새로운 Food 객체를 생성한 뒤, 이를 foodRepository.save() 메소드로 저장합니다. 이 때 @GeneratedValue(strategy = GenerationType.AUTO) 어노테이션이 지정되어 있지 않으면 JPA가 자동으로 기본 키를 생성하지 않기 때문에, id 필드의 값이 null인 상태로 저장됩니다. 따라서 id 값을 참조할 때 NullPointerException이 발생합니다.

따라서 @GeneratedValue(strategy = GenerationType.AUTO) 어노테이션을 사용하여 JPA가 자동으로 기본 키를 생성하도록 설정해야 합니다.

 

틀린 부분

@Getter
@NoArgsConstructor
@Entity
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

 

 

SQL문을 update했음에도 적용되지 않는 문제

에러

application.properties
spring.jpa.hibernate.ddl-auto=create

 

해결방법

spring.jpa.hibernate.ddl-auto=none 설정을 하면 애플리케이션이 시작될 때 자동으로 DDL 생성을 하지 않습니다. 즉, 테이블을 생성하는 SQL을 애플리케이션이 실행되기 전에 직접 수동으로 생성하고 테이블이 생성되어 있는 것을 전제로 합니다.

테스트 코드에서는 자동으로 테이블을 생성하고 초기 데이터를 넣어주는 등의 작업이 필요하므로 spring.jpa.hibernate.ddl-auto=none 설정을 하면 이러한 작업이 실행되지 않기 때문에 테스트 코드가 정상적으로 동작합니다. 반면에, spring.jpa.hibernate.ddl-auto=create나 spring.jpa.hibernate.ddl-auto=update 설정을 하면 테스트 코드가 정상적으로 동작하지 않을 수 있습니다. 이는 자동으로 테이블을 생성하거나 업데이트하게 되어, 초기 데이터가 누락되거나 테스트 코드에서 예상치 못한 동작을 할 가능성이 있기 때문입니다.

 

틀린부분

고쳐야하는점1)

./gradlew clean build

 

고쳐야하는점2)

application.properties
spring.jpa.hibernate.ddl-auto=none

 

Foreign Key가 객체인 도메인을 테스트 해야할때(=역직렬화 문제)

*역직렬화란?

직렬화는 객체를 바이트 형태 또는 문자열 형태로 변환하는 것으로, 일반적으로는 네트워크를 통해 데이터를 전송하거나, 파일로 저장하는 등의 용도로 사용됩니다.

반면에, 역직렬화는 직렬화된 데이터를 다시 객체로 변환하는 것으로, 네트워크에서 전송받은 데이터나 파일에서 읽어들인 데이터를 프로그램에서 사용할 수 있는 형태로 변환하는 데 사용됩니다.

 

*역직렬화 사용이유

첫째, 네트워크를 통해 전송되는 데이터나 파일로 저장되는 데이터를 객체로 사용하기 위해 역직렬화를 수행합니다.

예를 들어, 클라이언트에서 서버로 요청을 보낼 때, 요청 데이터를 직렬화하여 전송하고, 서버에서는 해당 데이터를 역직렬화하여 처리합니다. 이때, 역직렬화된 데이터를 기반으로 로직을 수행하거나, 데이터베이스에 저장하는 등의 작업을 수행할 수 있습니다.

둘째, 객체를 직렬화하여 저장해두었다가 나중에 역직렬화하여 사용하는 경우가 있습니다. 이는 객체를 메모리에 유지하지 않고도, 나중에 필요할 때 다시 사용할 수 있도록 하는 것입니다. 예를 들어, 어플리케이션에서 많은 양의 데이터를 다루는 경우, 이 데이터를 객체로 변환하여 파일에 저장해두었다가 필요할 때 역직렬화하여 사용할 수 있습니다. 이렇게 하면, 메모리 부담을 줄일 수 있으며, 성능을 향상시킬 수 있습니다.

 

상황

@Getter
public class MenuRequestDto {
    private final String storeName;
    private final String foodName;
    @Builder
    public MenuRequestDto(String storeName,String foodName) {
        this.storeName = storeName;
        this.foodName = foodName;
    }
}

 

에러 

MenuRequestDto에 생성자가 없어 cannot deserialize from Object value 하여 InvalidDefinitionException이 발생

Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.programmers.dto.menu.MenuRequestDto]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.programmers.dto.menu.MenuRequestDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]

...

 com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.programmers.dto.menu.MenuRequestDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

 

오류 부분

  mockMvc.perform(post("/menu/save")
                        .contentType(MediaType.APPLICATION_JSON)
                        .characterEncoding(StandardCharsets.UTF_8)
                        .content(json))
                .andExpect(status().isOk())
                .andDo(print());

 

해결방법

 

Jackson은 기본적으로 매핑 대상 클래스에서 인식 가능한 생성자를 찾아 사용합니다. 하지만, 생성자 인자명이 변경된 경우에는 Jackson이 이를 인식하지 못할 수 있습니다

이런 경우에는 @JsonProperty 어노테이션을 이용하여 매핑할 인자명을 지정해주는 방법이 있습니다. @JsonProperty 어노테이션을 생성자 인자 앞에 붙이고, 해당 인자와 매핑될 JSON 필드 이름을 인자로 지정해주면 됩니다.

 

@JsonProperty 어노테이션을 사용하면, JSON 데이터의 필드 이름과 객체의 필드 또는 생성자 인자 이름을 매칭할 수 있습니다. 이를 이용하여, 인자 이름이 변경되더라도 Jackson이 정상적으로 객체를 생성할 수 있게 됩니다.

 

@ManyToOne
@JoinColumn(name = "storeId", referencedColumnName = "storeId", foreignKey = @ForeignKey(name = "fk_menu_store"))
private Store store;

@ManyToOne
@JoinColumn(name = "foodId", referencedColumnName = "id", foreignKey = @ForeignKey(name = "fk_menu_food"))
private Food food;

 

 

*생성자 인자 이름과 메서드의 인자 이름이 다른 경우

1.예를 들어, 다음과 같은 클래스가 있다고 가정해보겠습니다.

public class MyClass {
    private String myField;

    public MyClass(String myField) {
        this.myField = myField;
    }

    public void setMyField(String myField) {
        this.myField = myField;
    }
}

2.다음과 같은 코드를 컴파일한 후, MyClass.class 파일을 열어보면,

public class MyClass {
    private String myField;

    public MyClass(String arg0) {
        this.myField = arg0;
    }

    public void setMyField(String arg0) {
        this.myField = arg0;
    }
}

이를 보면, 생성자와 메서드의 인자 이름이 myField에서 arg0으로 변경된 것을 확인할 수 있습니다.

이러한 변경은 컴파일러가 컴파일하는 동안 발생하며, 리플렉션과 같이 런타임에서 인자 이름을 사용하는 경우, 이러한 변경이 문제가 될 수 있습니다.

 

3.@JsonProperty 어노테이션을 사용하면, JSON 데이터의 필드 이름과 객체의 필드 또는 생성자 인자 이름을 매칭할 수 있습니다. 이를 이용하여, 인자 이름이 변경되더라도 Jackson이 정상적으로 객체를 생성할 수 있게 됩니다.

public class MyClass {
    @JsonProperty("myField")
    private String myField;

    public MyClass(@JsonProperty("myField") String myField) {
        this.myField = myField;
    }
}

 

*생성자 인자 이름과 Json 데이터 필드 이름이 다른 경우

1.다음과 같은 JSON 데이터가 있다고 가정해보겠습니다.

{
  "field": "Hello, World!"
}

2.생성자 인자 이름과 JSON 데이터의 필드 이름이 일치하지 않으므로, 역직렬화에 실패하게 됩니다.

public class MyClass {
    private String myField;

    public MyClass(String myField) {
        this.myField = myField;
    }
}

3.다음과 같이 @JsonProperty 어노테이션을 사용하여 매핑시켜준다면,

public class MyClass {
    private String myField;

    public MyClass(@JsonProperty("field") String myField) {
        this.myField = myField;
    }
}

4.이제 Jackson은 JSON 데이터의 otherField 필드 이름과 생성자 인자의 field 인자 이름을 매칭시켜주게 됩니다. 따라서, 다음과 같은 코드를 사용하여 객체를 역직렬화할 수 있습니다.

ObjectMapper objectMapper = new ObjectMapper();
MyClass myObject = objectMapper.readValue(jsonString, MyClass.class);

 

 

0개의 인수가 필요하지만 1개가 발견되었습니다

에러 

post사용시 "0개의 인수가 필요하지만 1개가 발견되었습니다" 라고 빨간줄뜸

  @Test
    void write() {
        SignUpRequestDto requestDtoMember = basicMemberData();
        UUID memberId = memberService.signUp(requestDtoMember);
        Member member = memberRepository.findById(memberId).orElseThrow();
        Post savedPost = postRepository.save(basicPostData(member), member);

        //when,then
        mockMvc.perform(post("/posts")
                        .param(basicPostData(member).getTitle(), savedPost.getTitle())
                        .param(basicPostData(member).getContent(), savedPost.getContent())
                        .param(basicPostData(member).getWriterId(), String.valueOf(savedPost.getWriterId()))
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                .andExpect(status().is3xxRedirection())
                .andExpect(view().name("redirect:/posts"))
                .andDo(print());

    }

오류 부분

 

 

해결방법

아래에 동일한 Post이름의 메소드가 있어서였다.....

@Test
void eachPost() throws Exception {
    SignUpRequestDto requestDtoMember = basicMemberData();
    UUID memberId = memberService.signUp(requestDtoMember);
    Member member = memberRepository.findById(memberId).orElseThrow();
    Post savedPost = postRepository.save(basicPostData(member), member);

    mockMvc.perform(get("/posts/{postId}/",savedPost.getPostId()))
            .andExpect(status().isOk())
            .andExpect(view().name("post"))
            .andDo(print());
}
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함