Как проверить, возвращает ли конечная точка REST число в Spring Boot?

У меня есть простой контроллер, который запрашивает число из какой-то случайной службы REST и заключает его в объект JSON. Эти числа могут быть либо целыми числами, либо числами с плавающей запятой. Таким образом, потребители моей конечной точки REST должны ожидать значение с плавающей запятой.

Это мой контроллер:

import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
public class NumberController {

    private final RestTemplate restTemplate;

    @Autowired
    public NumberController(final RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @RequestMapping(path = "/number", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public String getNumber() {
        final String number = restTemplate.getForObject("https://example.com/number", String.class);

        return String.format("{\"number\":%s}", number);
    }

}

Теперь я хочу проверить, действительно ли конечная точка возвращает число, которое возвращает вызов REST. Поэтому я написал тест, который использует MockMvc:

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.client.RestTemplate;

@SpringBootTest
@SpringJUnitWebConfig
@AutoConfigureMockMvc
class NumberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private RestTemplate restTemplate;

    @ParameterizedTest
    @MethodSource("createTestData")
    void testNumbersEndpoint(final String restServiceValue, final double expectedValue) throws Exception {
        given(restTemplate.getForObject(any(String.class), eq(String.class))).willReturn(restServiceValue);

        mockMvc.perform(get("/number"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("number", is(expectedValue)));
    }

    private static Stream<Arguments> createTestData() {
        return Stream.of(Arguments.of("17", 17.0), Arguments.of("12.53", 12.53));
    }

}

Таким образом, конечная точка может либо вернуть { "number": 17 }, либо { "number": 12.53 }, что является допустимым JSON. С помощью .andExpect(jsonPath("number", is(expectedValue))) я проверяю, действительно ли структура JSON содержит число, возвращенное удаленной службой REST. К сожалению, тест для { "number": 17 } не пройден, потому что jsonPath("number", ...) передает целочисленное значение сопоставителю.

Итак, как я могу сопоставить как целые числа, так и значения с плавающей запятой?

Я думал о чем-то вроде следующего, но это не работает:

@ParameterizedTest
@MethodSource("createTestData")
void testNumbersEndpoint(final String restServiceValue, final Number expectedValue) throws Exception {
    given(restTemplate.getForObject(any(String.class), eq(String.class))).willReturn(restServiceValue);

    mockMvc.perform(get("/number"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("number", is(expectedValue)));
}

private static Stream<Arguments> createTestData() {
    return Stream.of(Arguments.of("17", BigDecimal.valueOf(17)), Arguments.of("12.53", BigDecimal.valueOf(12.53)));
}

person stevecross    schedule 17.05.2018    source источник


Ответы (1)


Базовый синтаксический анализатор JSON (который по умолчанию является JsonSmart) выберет «наиболее подходящий» тип данных для представления чисел. Выбранный вами подход почти работает, вам просто нужно сопоставить фактические типы данных, которые создает синтаксический анализатор JSON. В вашем примере int и double. Так

private static Stream<Arguments> createTestData() {
    return Stream.of(Arguments.of("17", 17), Arguments.of("12.53", 12.53));
}

должен работать.

Это возможно, потому что входные значения известны заранее, а также известно поведение анализатора JSON. Если вам нужно сопоставить произвольные типы чисел, вы можете реализовать свой собственный Matcher, который выполняет некоторое преобразование условного типа данных:

import java.math.BigDecimal;
import java.math.BigInteger;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

static Matcher<Number> jsonNumber(final BigDecimal d) {
    return new TypeSafeDiagnosingMatcher<Number>() {
        @Override
        public void describeTo(Description description) {
            description.appendText("a numeric value equal to ").appendValue(d);
        }

        @Override
        protected boolean matchesSafely(Number item, Description mismatchDescription) {
            BigDecimal actual;
            if (item instanceof BigDecimal) {
                actual = (BigDecimal) item;
            } else if (item instanceof BigInteger) {
                actual = new BigDecimal((BigInteger) item);
            } else {
                actual = BigDecimal.valueOf(item.doubleValue());
            }

            if (d.compareTo(actual) == 0) {
                return true;
            }

            mismatchDescription.appendText("numeric value was ").appendValue(item);
            return false;
        }
    };
}
person Tom    schedule 23.05.2018