1

I have a simple controller that requests a number from some random REST service and wraps it in a JSON object. These numbers can either be integers or floating point numbers. Thus the consumers of my REST endpoint should expect a floating point value.

This is my controller:

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);
    }

}

Now I want to test if the endpoint really returns the number that the REST call returns. Therefore I have written a test that uses 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));
    }

}

So the endpoint can either return { "number": 17 } or { "number": 12.53 } which is both valid JSON. With .andExpect(jsonPath("number", is(expectedValue))) I test if the JSON structure really contains the number that was returned by the remote REST service. Unfortunately the test fails for { "number": 17 }, because jsonPath("number", ...) passes an integer value to the matcher.

So how can I match both integer an floating point values?

I was thinking of something like the following, but it doesn't work:

@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)));
}
stevecross
  • 5,588
  • 7
  • 47
  • 85

1 Answers1

0

The underlying JSON parser (which is JsonSmart by default) will choose the "most appropriate" data type to represent numbers. The approach you chose is nearly working, you just have to match the actual data types that the JSON parser produces. In your example int and double. So

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

should be working.

This is possible because the input values are known in advance and the behavior of the JSON parser is also known. If you need to match arbitrary number types, then you might implement your own Matcher that does some conditional data type conversion:

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;
        }
    };
}
Tom
  • 515
  • 7
  • 9