0

In a spring mvc app, I submit id's and use a formatter to convert that id to an object. It works well in the container.

But in the unit test environment, I'm seeing a problem.

I mock the formatter to always return my test value, this is fine it gets injected into the ModelAttribute. But in the BindingResult, a call to result.getFieldValue("location") for example is returning null, but only in the MockMvc context.

This is the test case:

/**
 * Tests the inventory update for existing inventory records.
 * @throws Exception
 */
@Test
public void testUpdateExistingProductInventory() throws Exception{
    logger.entry();
    VariantInventory oldInventory = new VariantInventory();
    oldInventory.setId(20l);

    Product product = ProductBuilder.buildBasicExisting();
    Location location = new Location();
    location.setId(3l);
    ProductVariant variant = new ProductVariant();
    variant.setId(2l);

    // check the formatter is working
    Mockito.when(mockProductFormatter.parse(((String)Mockito.anyObject()), ((Locale)Mockito.anyObject()))).thenReturn(product);
    Product p = mockProductFormatter.parse("1", null);
    Assert.assertEquals(p, product);

    // check the formatter is working
    Mockito.when(mockLocationFormatter.parse(((String)Mockito.anyObject()), ((Locale)Mockito.anyObject()))).thenReturn(location);
    Location l = mockLocationFormatter.parse("3", null);
    Assert.assertEquals(l, location);

    // check the formatter is working
    Mockito.when(mockVariantFormatter.parse(((String)Mockito.anyObject()), ((Locale)Mockito.anyObject()))).thenReturn(variant);
    ProductVariant pv = mockVariantFormatter.parse("2", null);
    Assert.assertEquals(pv, variant);

    // check the formatter is working
    Mockito.when(mockInventoryFormatter.parse(((String)Mockito.anyObject()), ((Locale)Mockito.anyObject()))).thenReturn(oldInventory);
    VariantInventory v = mockInventoryFormatter.parse("20", null);
    Assert.assertEquals(v, oldInventory);

    this.mockMvc.perform(MockMvcRequestBuilders.post("/ajax/products/update/inventory")
            .param("product", "1")
            .param("variant", "2")
            .param("location", "3")
            .param("status", "ACTIVE")
            .param("quantityOnHand", "30.5")
            .param("lowStockQuantity", "10")
            .param("inventory", "20")
            )
            .andExpect(status().isOk());

    Mockito.verify(mockInventoryService, Mockito.times(1)).updateExisting(Mockito.eq(oldInventory), Mockito.any(VariantInventory.class));

    logger.exit();
}

This is the relative part of the controller:

 @RequestMapping(value = "/ajax/products/update/inventory", method= RequestMethod.POST)
    public @ResponseBody
    AJAXResponse updateProductInventory(@ModelAttribute ProductInventoryFormWrapper formWrapper, BindingResult result,
                                         ModelMap map) {
        logger.entry();
        logger.debug("Getting product data");

        if (!result.hasErrors()) {
            inventoryValidator.validate(formWrapper, result);
        }
}

Then skipping a few items, this is the relevant validation that fails, where I am passing location as the field.

ValidationUtils.rejectIfEmptyOrWhitespace(errors, field, "required.field", new String[]{label});

The object fails to validate because of what must be a bug.

What I observe if I debug the controller is:

  1. The object is in the FormWrapper, and the properties are there.
  2. But in the BindingResult object, if I call 'getFieldValue('location')` which is what's being called in the spring validation code, it's returning null, and therefore the validator rejects the value.

So for some reason the binding result hasn't registered the formatted fields or something. Note that this only happens in the Unit Test, not in the container.

Does anyone know how to fix?

Quick Edit:

I've done some more debugging, and it's failing in this block of code from AbstractPropertyBindingResult. The value is okay right up until the conversionService is called to convert it. I haven't downloaded the source beyond that method, so I can't see exactly why it's failing, but somewhere in the convert method it's being turned from the proper value, to null. I presume because I'm using MockObjects, and maybe it's calling something that I haven't anticipated to return the value.

@Override
    protected Object formatFieldValue(String field, Object value) {
        String fixedField = fixedField(field);
        // Try custom editor...
        PropertyEditor customEditor = getCustomEditor(fixedField);
        if (customEditor != null) {
            customEditor.setValue(value);
            String textValue = customEditor.getAsText();
            // If the PropertyEditor returned null, there is no appropriate
            // text representation for this value: only use it if non-null.
            if (textValue != null) {
                return textValue;
            }
        }
        if (this.conversionService != null) {
            // Try custom converter...
            TypeDescriptor fieldDesc = getPropertyAccessor().getPropertyTypeDescriptor(fixedField);
            TypeDescriptor strDesc = TypeDescriptor.valueOf(String.class);
            if (fieldDesc != null && this.conversionService.canConvert(fieldDesc, strDesc)) {
                return this.conversionService.convert(value, fieldDesc, strDesc);
            }
        }
        return value;
    }
Richard G
  • 5,243
  • 11
  • 53
  • 95

1 Answers1

0

Ok that was a tough one, so I didn't really expect anyone to answer. But here's the answer. I was right, the Mock was being called in the validation. So I had to add an additional mock method to the formatters (print):

// check the formatter is working
Mockito.when(mockInventoryFormatter.parse(((String)Mockito.anyObject()), ((Locale)Mockito.anyObject()))).thenReturn(oldInventory);
// this was added  
Mockito.when(mockInventoryFormatter.print(Mockito.any(VariantInventory.class), Mockito.any(Locale.class))).thenReturn("20");
Richard G
  • 5,243
  • 11
  • 53
  • 95