안녕하세요. 회사와 함께 성장하고 싶은 Kose입니다.

오늘은 크리스마스 이브입니다. 모두 행복한 크리스마스 보내시기 바랍니다.!
이번 글은 Spring에서 Mockito 라이브러리와, Postman을 활용한 Controller 테스트 코드 작성에 대해 논의해보고자 합니다.

Mock
모의 객체(Mock Object)란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는 데 사용하는 객체입니다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용됩니다. 위키백과, Mock Object 인용

 

평소에 테스트 코드를 짤 때, 눈으로 직접 확인하고 계속 틀려보고 오류를 수정하는 작업을 하면서 많이 성장할 수 있다고 생각하였습니다. 하지만, 외부의 요청이 있어야 하는 경우, 직접 코드를 작성하는 데 한계가 있을 수 있습니다. 이럴 때, Mock 객체를 활용하여 가짜 요청을 생성한 후, 작성한 코드의 품질을 평가한다면 코드 작성의 효율이 높아질 수 있습니다.

저는, 학과 내 개발에 관심이 있는 학우와 팀을 구성하여 간단한 메신저 프로젝트를 진행하였습니다. 백엔드를 구성하는 과정에서 작성한 로직을 바탕으로 예시를 작성하겠습니다.

1. SchoolController 구성

@RestController
@Slf4j
@RequestMapping("/api/v1/schools")
@RequiredArgsConstructor
public class SchoolController {

    private final SchoolService schoolService;

    @GetMapping("/search")
    public ResponseEntity getSchoolsSearchRes(@RequestHeader("Authorization") String accessToken,
                                              @RequestBody SchoolSearchReqDto schoolSearchReqDto) {

        Page<SchoolSearchDto> schoolSearchDto = schoolService.getSchoolSearchDto(schoolSearchReqDto);

        return new ResponseEntity(schoolSearchDto, HttpStatus.OK);
    }

}

해당 컨트롤러는 AccessToken을 Authorization 헤더로 받고, School을 검색하기 위한 SearchReqDto를 json 형태로 받아, QueryDsl을 이용하여 페이징 처리 하여 값을 받아오는 컨트롤러입니다.

2. TestClass 작성

@SpringBootTest
@Transactional
@ExtendWith(MockitoExtension.class)
class SchoolControllerTest {

    @Autowired UserService userService;
    @Autowired AuthService authService;
    @Autowired AuthorityService authorityService;
    @Autowired PasswordEncoder passwordEncoder;
    @Autowired SchoolService schoolService;
    @Mock SchoolController schoolController;

    static Users user;
    static List<Authority> authorities;
    static TokenAuthDto tokenAuthDto;
    static String accessToken;

    private MockMvc mockMvc;

해당 컨트롤러는 로그인이 필요하므로,
유저 가입 - userService (유저 패스워드 인코딩 - passwordEncoder)
권한 부여 - authorityService
토큰 발급 - authService
학교 찾기 - schoolService
빈으로 등록된 클래스를 의존성 주입받습니다.

그리고, Mock 라이브러리로 테스트가 필요한 SchoolController를
Mock빈 주입 받도록 합니다.

3. BeforeEach method

@BeforeEach
public void dbInit() {
        userService.registerForm(FormRegisterUserDto.builder().email("k@naver.com").username("kose").password("1234").build(), passwordEncoder);
        user = userService.findByEmail("k@naver.com");
        authorities = authorityService.findAuthorityByUser(user);

        tokenAuthDto = authService.createFormTokenAuth("k@naver.com", authorities);
        accessToken = "Bearer " + tokenAuthDto.getAccessToken();

        mockMvc = MockMvcBuilders.standaloneSetup(schoolController).build();

        saveSchool();
    }
    
private void saveSchool() {

        String[] school = {"서울A대학교", "서울B대학교", "인천A대학교"};
        String[] address = {"서울시 A구", "서울시 B구", "인천시"};
        String[] regisNum = {"123", "124", "125"};

        for (int i=0; i< school.length; i++) {
            schoolService.save(new SchoolSaveDto(school[i], address[i], regisNum[i]));
        }
    }
    
    private SchoolSearchReqDto requestDto() {
        return SchoolSearchReqDto.builder().page(0).size(10).schoolName("서울").build();
    }    

@BeforeEach 빈은, 테스트 클래스에 작성된 개별 Test 메소드가 실행되기 전에 실행되어 각 테스트의 개별화를 돕습니다.

해당 dbInit() 회원가입 및 로그인 로직, 학교 데이터 저장을 위해 설정한 메소드입니다.
사용자가 ID/PASSWORD 로그인을 이용하면, 회원 가입 시 "ROLE_USER"을 부여받고 로그인에 성공하여 토큰을 부여받습니다. accessToken은 Http Get 요청에 Header로 사용하기 위해 "Beaer "를 포함한 문자열로 static accessToken에 할당합니다.

MockMvc는 웹 애플리케이션을 애플리케이션 서버에 배포하지 않고도 스프링 MVC의 동작을 재현할 수 있는 클래스입니다.
MockMvcBuilders.standaloneSetup().build()를 활용하여 MockMvc 클래스의 인스턴스를 static mockMvc에 할당합니다.

3. 학교_검색 method

@Test
    public void 학교_검색() throws Exception {

        //given
        SchoolSearchReqDto reqDto = requestDto();
        Page<SchoolSearchDto> resDto = schoolService.getSchoolSearchDto(reqDto);

        //when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.get("/api/v1/schools/search")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new Gson().toJson(reqDto))
                        .header("Authorization", accessToken)
        );

        //then
        resultActions.andExpect(status().isOk());

    }

해당 메소드는, 실제 테스트를 구현하는 메소드입니다.

Given
requestDto() 메소드를 호출하여 reqDto를 생성한 후, 의존성 자동 주입을 설정한 schoolService의 gerSchoolSearchDto() 메소드를 호출 합니다. 해당 메소드는 학교 이름, 학교 주소 파라미터를 동적으로 입력받아 학교 관련된 학교를 제공하는 QueryDsl을 활용한 동적 쿼리입니다.

When
MockRequestBuilders.Get()은 mockMvc를 수행할 때, 요청하는 Http method입니다. 컨트롤러에 GetMapping으로 설정하였으므로, MockMvcRequestBuilders의 httpMethod를 설정하는 메소드도 get으로 설정하여야 합니다.

contentType은 클라이언트에게 json으로 입력받기 때문에 MediaType.APPLICATION_JSON을 설정합니다.

중요한 부분은 Header입니다. 처음 Controller에서
@RequestHeader("Authorization) String accessToken 을 설정하였습니다.
해당 메소드가 실행되기 위해서는 header에 유효한 accessToken을 Authorization에 설정하여 HttpRequest 요청을 수행해야 합니다.

then
andExpect()는 해당 요청에 대한 상태 및 content 결과를 검증할 수 있는 결과 메소드 입니다. 저는 해당 요청에 대한 성공 상태코드로 200을 설정하였으므로 isOK()로 설정하였습니다. 그뿐만 아니라, andExpect()로 content 값도 확인할 수 있는데, 타입 에러가 발생하여…. 이 부분은 Mock을 더 공부한 후에 적용하도록 하겠습니다.

 

4. Postman 검증

서버를 가동한 후, 회원가입 -> 로그인 -> 학교 찾기 Http 요청 순서로 진행하였습니다.

먼저, json 형태로 회원 가입 요청을 수행합니다.


회원가입에 성공 후, 로그인을 요청합니다.


Tests 목록에 해당 요청에 대한 결과를 테스트 코드 형태로 작성할 수 있습니다.
저는 요청 결과 상태코드 pm.response.to.have.status(200)으로 검증하였습니다.

var data = JSON.parse(responseBody);
pm.environment.set("accessToken", data.accessToken)
pm.environment.set("refreshToken", data.refreshToken)

해당 코드는 서버에서 전달한 Token 결과를 클라이언트의 Header에 저장하기 위해 설정한 코드입니다. 백엔드에서 프론트로 Token을 제공하면, 클라이언트는 해당 토큰을 쿠키나 세션 로컬 스토리지 등에 저장합니다.
postman은 environment.set을 호출하여 key-value 형태로 저장하여 다음 요청에 이를 활용할 수 있습니다.

학교 찾기 요청을 위해 Authorization 목록의 Type을 Bearer Token으로 설정한 후 {{key}}를 설정하면 위에 설정한 accessToken을 jwt 토큰으로 이용합니다.


헤더 설정 후, json으로 page, size, schoolName을 설정하여 요청을 보냅니다.


postman에서 원했던 결과를 확인할 수 있습니다.

 

5. 추가 공부할 내용

mock 객체를 활용하여 테스트 코드를 작성하면서 에러가 발생했던 부분이 있습니다.

resultActions.andExpect(status().isOk());

이 부분에 다른 추가 검증 내용을 작성하고 싶은데, ResultActions 클래스의 메소드 요청 파라미터를 올바르게 이해한 후에 추가 검증을 구현하여 글로 남기겠습니다.

또한, @Mock을 적용한 서비스 로직, @Mock을 적용한 서비스를 주입받고자 하는 @InjectMocks을 선언한 컨트롤러에 doReturn().when()을 적용할 때, 아래 에러가 발생하였습니다.

please remove unnecessary stubbings or use 'lenient' strictness. more info: javadoc for unnecessarystubbingexception class.

해당 원인은 불필요한 스터빙을 하지 않도록 되어있는데, 현재 코드에 쓰이지 않는 스터빙을 해놨기 때문에 발생한 에러입니다. 이에 대한 해결책으로 Mock 객체를 활용하지 않아도 되는 Service는 의존성 주입을 받아 진행했습니다. 이 해결책이 올바른지 궁금하여 추가 공부를 더 진행하고자 합니다.

오늘도 즐거운 공부 시간이었습니다.
감사합니다.!

+ Recent posts