mockMVC test code I wrote call other controller's api (maybe)

178 views Asked by At

First I ask for your understanding of my poor eng.

I'm writing test code for make api specification by Spring Rest Docs library.

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.10'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

group = 'com.jogijo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

compileJava.options.encoding = 'UTF-8'

configurations {
    asciidoctorExt
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.vladmihalcea:hibernate-types-52:2.17.3'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA
    implementation 'org.apache.httpcomponents:httpcore:4.4.15'
    implementation 'org.apache.httpcomponents:httpclient:4.5.13'

    //implementation 'org.springframework.boot:spring-boot-starter-log4j2'
    implementation 'jakarta.xml.bind:jakarta.xml.bind-api:2.3.2'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'

    // 유효성
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // model struct
    implementation 'org.mapstruct:mapstruct:1.5.4.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.4.Final'

    // security
    implementation('org.springframework.boot:spring-boot-starter-security')

    // DB
    //runtimeOnly ('mysql:mysql-connector-java:8.0.32') //mysql8
    runtimeOnly("com.mysql:mysql-connector-j")
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //implementation'mysql:mysql-connector-java'


    // mybatis
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.0'

    // aws s3
    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

    //jwt
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'


    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // Validation
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    //oauth
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

    // Spring Rest Docs
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

ext {
    snippetsDir = file('build/generated-snippets')
}

tasks.named('test') {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'
    dependsOn test

}

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    doFirst{
        delete file('src/main/resources/static/docs')
    }
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

build {
    dependsOn copyDocument
}

bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}

this is build.gradle

@RestController
@RequestMapping("/app/alarms")
@RequiredArgsConstructor
public class AlarmController {
    private final AlarmService alarmService;
    private final AlarmProvider alarmProvider;
    private final JwtService jwtService;

    /**
     * 알람 1개 불러오기
     * @param alarmId
     * @return
     */
    @GetMapping("/{alarmId}")
    public BaseResponse<AlarmRes> GetAlarm(@PathVariable Integer alarmId){
        AlarmRes alarmRes = alarmProvider.getAlarm(alarmId);

        return new BaseResponse<>(ResponseStatus.SUCCESS, alarmRes);
    }

this is the api that I want to test.

package com.wakeUpTogetUp.togetUp.alarms;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wakeUpTogetUp.togetUp.alarms.dto.response.AlarmRes;
import com.wakeUpTogetUp.togetUp.common.ResponseStatus;
import com.wakeUpTogetUp.togetUp.common.dto.BaseResponse;
import com.wakeUpTogetUp.togetUp.utils.JwtService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(AlarmController.class)
@AutoConfigureRestDocs
class AlarmControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @MockBean
    private AlarmService alarmService;
    @MockBean
    private AlarmProvider alarmProvider;
    @MockBean
    private JwtService jwtService;

    @Test
    @DisplayName("getAlarm - [Get] /alarm/{alarmId}")
    void getAlarm() throws Exception{
        //given
        AlarmRes response = AlarmRes.builder()
                .id(42)
                .userId(9)
                .missionId(1)
                .name("기상알람")
                .icon("⏰")
                .sound("default")
                .volume(80)
                .isVibrate(true)
                .isRoutineOn(true)
                .snoozeInterval(5)
                .snoozeCnt(3)
                .startHour(6)
                .startMinute(0)
                .monday(true)
                .tuesday(true)
                .wednesday(true)
                .thursday(true)
                .friday(true)
                .saturday(true)
                .sunday(false)
                .isActivated(true)
                .build();

        given(alarmProvider.getAlarm(42)).willReturn(response);

        Integer alarmId = 42;

        //when
        ResultActions action = mockMvc.perform(get("/app/alarms/42"))
                .andDo(print());

        //then
        BaseResponse<AlarmRes> responseData = new BaseResponse<>(ResponseStatus.SUCCESS, response);

        action.andExpect(status().isOk())
                .andExpect(content().json(objectMapper.writeValueAsString(responseData)))
                .andExpect(jsonPath("$.result.userId").value(9))
                .andDo(
                        // rest docs 문서 작성 시작
                        document("alarm/getAlarm", // directory명 위에서 설정한 build/generated-snippets 하위에 생성
                                // --> build/generated-snippets/member/create
                                requestParameters( // queryString 관련 변수 정보 입력
                                        parameterWithName("alarmId").description("알람 Id")
                                ),
                                responseFields( // response data 필드 정보 입력
                                        fieldWithPath("httpStatusCode").description("http 상태코드"),
                                        fieldWithPath("httpReasonPhrase").description("http 상태코드 설명문구"),
                                        fieldWithPath("message").description("설명 메시지"),
                                        subsectionWithPath("result").description("결과"),
                                        fieldWithPath("id").description("알람 Id"),
                                        fieldWithPath("userId").description("사용자 Id")
                                )
                        )
                );
    }

this is test code.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /app/alarms/42
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost:8080/app/alarms/42]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

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

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 302
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Location:"http://localhost:8080/oauth2/authorization/google"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = [HERE IS REDIRECT URL BUT IT COULD BE SPAM POST. LOCALHOST:8080 / OAUTH2
AUTHORIZATION / GOOGLE]
          Cookies = []

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /app/alarms/42
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost:8080/app/alarms/42]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

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

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 302
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Location:"http://localhost:8080/oauth2/authorization/google"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = http://localhost:8080/oauth2/authorization/google
          Cookies = []

Status expected:<200> but was:<302>
Expected :200
Actual   :302
<Click to see difference>

java.lang.AssertionError: Status expected:<200> but was:<302>

AlarmControllerTest > getAlarm - [Get] /alarm/{alarmId} FAILED
    java.lang.AssertionError at AlarmControllerTest.java:79
1 test completed, 1 failed
> Task :test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///C:/Users/anyti/IdeaProjects/TogetUp/build/reports/tests/test/index.html
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 9s
4 actionable tasks: 2 executed, 2 up-to-date

This is error code.

There is google oauth login code that redirect to url. But I don't know why this test code get that response. They're in different controller and also there's no api that reply the url "http://localhost:8080/oauth2/authorization/google"

I annotated all code related with oauth but It didn't work. and I check the url but It's right I think.

I'm really exhausted now. I would so glad if someone help me.

1

There are 1 answers

0
ch4mp On

The security configuration is missing in your question, but you obviously have configured your application as an OAuth2 client (with Google as authorization server), and redirection to login for requests having a session which do not have an authorized client.

You are redirected to login because you didn't set or mock the security context for your test request. I suggest you read this Baeldung article I wrote.

In your case, what might be needed is:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login;

...
    get("/app/alarms/42").with(oauth2Login()/* configure user identity details there */)

Another option is to use this libs I wrote and decorate your tests methods or classes with @WithOAuth2Login or @WithOidcLogin.