[TIL] - MockMVC와 NullPointException

김주형·2022년 8월 17일
0

TIL

목록 보기
10/37
post-thumbnail

작성 목적

오늘.. 해서는 안 되는 "그" 실수를 하고 말았다.

꽤 공들여 작성한 코드인데..
좀 괜찮은 코드 같은데..
어쩌면.. 이번엔 디버깅 버튼은 생략하고..
대신 실행버튼을 눌러봐도 될 지도..? ㅎ

테스트 실패

로그를 읽어보고,
코드를 검토해보고, 구글링을 아무리 하더라도
에러를 해결하고나서 가장 중요하다고 느낀 것은

"객관적으로 나의 코드가 정말 올바른지 검토하는 것"
이 부족했구나.. 역시 나의 논리에 대해 더 신중함을 가져야 하는 구나..
라고 나의 한계와 내 코드가 작동하지 않는 이유의 실체를 마주했다.
정말 겸손해야겠다.

같은 실수를 더 이상 반복하지 않기 위해
MockMvc 사용 방법과
가장 기초적인 NullPointException의 발생과정에 대해 취득한 정보를 기록하려고 한다.


NullPointException 발생 과정

NullPointException 공식 설명

public class NullPointerException
extends RuntimeException

객체가 필요한 경우에 어플리케이션이 null을 사용하려고 하면 발생 됩니다.
Thrown when an application attempts to use null in a case where an object is required.
These include:

  • Calling the instance method of a null object.
  • Accessing or modifying the field of a null object.
  • Taking the length of null as if it were an array.
  • Accessing or modifying the slots of null as if it were an array.
  • Throwing null as if it were a Throwable value.
  • Applications should throw instances of this class to indicate other illegal uses of the null object.
  • NullPointerException objects may be constructed by the virtual machine as if suppression were disabled and/or the stack trace was not writable.

무슨 소린지 이해해보자

  • 레퍼런스 변수를 선언하면 객체의 포인터가 생성된다.
    (레퍼런스 변수는 객체의 주소 (참조값)을 저장)
    예를 들어 primitive 타입 변수 선언 시
int x;
  • 변수 x는 int 타입으로 선언되어, 자바는 이 변수의 값을 0으로 초기화 (당연히 변수 x가 클래스의 필드로 정의되었다고 가정)
x = 10;
  • 해당 변수 x에 10이라는 값을 대입 연산자를 사용하여 할당하면,
    변수 이름 x가 가리키는 메모리 위치에 10이라는 값이 쓰여지게 된다.

하.지.만

레퍼런스 타입 변수 선언 시 자바의 처리방법은 다르다!

Integer num; // 1
num = new Integer(10); // 2
  1. num이라는 이름으로 선언된 변수는 원시(primitive) 값을 저장하지 않는다.
    대신, Integer라는 이름의 타입은 래퍼클래스로서, 레퍼런스타입이므로 해당 변수는 주소 (참조값)을 저장하게 된다.
    -> 아직 어떤 것을 참조하라고 정의하지 않았기 때문에 자바는 그 변수를 null로 초기화한다.
    -> "나는 아무것도 참조하지 않아!"라는 의미

  2. new라는 키워드를 사용해서 Integer 클래스 객체를 생성하고 해당 객체의 주소 (참조값을) num이라는 변수에 저장하면,
    객체를 생성하고 객체의 참조값을 저장한 변수를 사용하여 해당 객체에 접근할 수 있게 된다. (그래서 이 때, . 연산자를 사용)

만약 레퍼런스 타입 변수를 선언하고 객체를 생성하지 않으면 (즉, 객체의 참조값을 해당 변수에 저장하지 않으면) Exception이 발생
-> 객체가 생성되기 전 num 변수를 사용해서 해당 클래스의 객체를 접근하고자 하면 NullPointerException이 발생!
이러한 경우 대부분 컴파일러가 해당 문제를 인식해서 경로 메시지로 알려주게 된다. "num 변수가 아직 초기화되지 않았어"라고 ..

만약 이런 메서드를 다음과 같이 호출시

public void doSomething(Integer num) { // do something to num }

public static void main(String[] args){
	doSomething(null);
}

num 변수의 값은 뭐가 될까?

null 값을 가지는 num 변수를 사용해서 객체의 필드 혹은 메서드에 접근하고자 한다면 NullPointerException이 발생하게 되는 원리였다..
이와 같은 exception이 발생되지 않게 하는 최선의 방법은
레퍼런스 변수를 사용하기 전에 null 값을 저장하고 있는지를 체크하는 것이라고 한다.

public void doSomething(Integer num) {
	if(num != null) { 
    // do something to num 
    } 
 }

MockMVC

내가 사용하는 객체가 초기화 되지 않는 경우를 검증한 뒤에도
MockMVC 사용에 대한 지식이 없어서 삽질을 2시간 정도 했다..

공식 주석은 다음과 같다.

"Main entry point for server-side Spring MVC test support."

//Example
  import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
 
  // ...
 
  WebApplicationContext wac = ...;
 
  MockMvc mockMvc = webAppContextSetup(wac).build();
 
  mockMvc.perform(get("/form"))
      .andExpect(status().isOk())
      .andExpect(content().mimeType("text/html"))
      .andExpect(forwardedUrl("/WEB-INF/layouts/main.jsp"));

아주 간결하고 군더더기 없는 설명하지만 좀 더 음미하기 위해 배경을 살펴보자면,

  • 테스트 코드를 작성하지 않을 시, request를 발생시킬 수 있는 도구(ex. Postman..)를 사용해 직접 호출해 서버를 디버깅해야함
  • MockMVC를 사용하면 이 과정 생략 가능
  • 즉, Controller 호출 도구
  • 공식적으론 "Spring MVC 테스트 유틸리티 클래스"

필요에 따른 설정 방법

컨트롤러를 테스트하는데 사용되는
어노테이션을 통한 2가지 세팅 방법을 알게 되었다.

  • @SpringBootTest : Bean으로 등록된 객체를 모두 메모리에 올린다. (= ApplicationContext 전체를 호출)
  • @WebMvcTest : 테스트에 필요한 Bean을 직접 세팅해 사용 (= 테스트에 필요한 레이어만 지정)

종류와 사용법

@RunWith

  • JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션.
  • WebApplicationContext를 생성, 관리하는 작업을 해당 어노테이션 안에 선언된 클래스로 이용한다는 의미

JUnit 5.x 부터는 @RunWith가 아닌 @ExtendWith를 사용한다.

  • 예시 코드
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class ApplicationTests {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    WebApplicationContext webApplicationContext;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .alwaysDo(print())
                .build();
    }

    @Test
    public void testCodeSample() throws Exception {
        mockMvc.perform(get("/address")
                        .param("param_1", "0"))
                .andExpect(status().isOk())
                .andExpect(content().string("Need reponse text"))
                .andReturn();
    }
    
    . . .
}
  • MockMVC 사용에 필요한 의존성을 제공한다.
  • 해당 어노테이션을 선언 후 MockMVC를 주입받아 사용한다.

테스트 함수 실행전 공통으로 적용할 요소들을 셋업한다. alwayDo(print()) 체이닝 함수는, 모든 테스트 함수마다 결과를 자세하게 출력하게끔 설정해 놓은 것이다.

JUnit 5.x에서는 @BeforeEach를 사용한다. @After, @AfterEach는 실행후 처리에 대해 지정할 수 있다.

  • @Test 어노테이션은 해당 함수가 테스트 함수임을 의미한다.
  • perform()부터는 본격적인 테스트 코드이다.
  • perform()에는 request method와 주소를 파라미터로 넣어준다. param(), content() 함수를 체이닝해 필요한 파라미터를 세팅할 수 있고 accept(), contentType()의 체이닝 함수를 사용해 필요와 목적에 따라 RequestBuilder를 사용할 수 있다.
get(/address”).param()
post(/address”).content()
andExpect()Permalink

perform()으로 발생한 요청에 대한 응답 어떤 기대값을 가져야하는지 지정한다.

var response = mockMvc.perform(get("/address")
                        .param("parma_1", "0"))
                .andExpect(status().isOk())
                .andExpect(content().string("Need reponse text"))
                .andDo(print())
                .andReturn();
                

위 샘플 코드는 Http Response Code가 200인지,
응답 content에 “Need reponse text” 문자열이 있는지 검증한다.
또한 andExpect(jsonPath(“$.field”).value(“success”))처럼 응답 JSON의 특정 경로에 원하는 값이 있는지 확인 가능하다.

MockMVC.perform()으로 리턴되는 ResultAction 인터페이스에 대한 처리를 지정한다.
보통 andDo(print())를 많이 쓰는데,
많이 쓰는 만큼 @Before와 setUp() 함수에서 미리 지정해놓으면 편하다고 한다.
andReturn()은 응답 객체를 그대로 재사용할 수 있도록 해준다.


Reference

다음을 참조하여 작성하였습니다. 정말 감사합니다. 🙇🏻‍♂️

profile
왔을때보다좋은곳으로

0개의 댓글