0

I have some logic for validation as follows

public interface IValidation {
   void validate();
}

public class ParameterValidator {
   public void validate(IValidation... validations) {
      for (IValidation validation : validations) {
        validation.validate();
      }
   }
}

One of the validation is on StringFormat as follows

public class StringFormatValidation implements IValidation {
   public StringFormatValidation(StringFormatValidator stringFormatValidator, String param) {
      ...
   }

   @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (!(obj instanceof StringFormatValidation)) return false;
        StringFormatValidation other = (StringFormatValidation) obj;
        if (!Objects.equals(this.param, other.param)) return false;
        return 
     Arrays.equals(SerializationUtils.serialize(this.stringFormatValidator), 
     SerializationUtils.serialize(other.stringFormatValidator));
}

}

where StringFormatValidator is a functional interface as follows

@FunctionalInterface
public interface StringFormatValidator extends Serializable {
    boolean apply(String arg);
}

I have overriden the equals to compare lambda on serialized bytes(not sure of any other better way as of now). I have a following unit test which works as expected

@Test
public void testEquality() {
  StringFormatValidation testFormatValidation1 = new 
  StringFormatValidation(StringFormatValidators::isCommaSeparated,"test1");
  StringFormatValidation testFormatValidation2 = new 
  StringFormatValidation(StringFormatValidators::isCommaSeparated,"test2");;
  Assert.assertEquals(testFormatValidation1, testFormatValidation2);
}

But when I am trying to test the call site as follows,

@MockBean
ParameterValidator parameterValidator;

@Captor
ArgumentCaptor<IValidation> argumentCaptor;

@Test
public void testParameterValidations() {
    testResource.doSomething(parameter1, "testParam");
    Mockito.verify(parameterValidator).validate(argumentCaptor.capture());
    List<IValidation> actualValidationList = argumentCaptor.getAllValues();
    StringFormatValidation testFormatValidation = new 
    StringFormatValidation(StringFormatValidators::isCommaSeparated, 
    "testParam");
    Assert.assertTrue(actualValidationList.contains(testFormatValidation));
}

I get java.io.NotSerializableException: Non-serializable lambda exception for the StringFormatValidation value in argument captor.

I don't understand how the captured value in Mockito's argument caprtor looses it's serializable behaviour, given that it's not a mocked value but actually created in the call site.

Note: I have simplified the overall signatures and naming for keeping the focus only at the problem at hand.

nishantv
  • 643
  • 4
  • 9
  • 27
  • Please note that I did not try the actual code, this is based solely on your comments and a visual review: **class** `StringFormatValidation` implements (mistakenly written extends?!) `IValidation` which does not extend `Serializable`. Are you perhaps mistaking it for **interface** `StringFormatValidator` which does indeed extend `Serializable`?! – Morfic Mar 20 '19 at 13:41
  • Thanks for taking the time to take a look into it. I have corrected the post to replace extends with implements. I did not provide `Serializable` to `IValidation` as I only need to serialize the `StringFormatValidator` functional interface which is used as an argument to supply a lambda instead. I then need to serialize this lambda in my `equals` method. – nishantv Mar 20 '19 at 14:38
  • You mentioned getting a `NotSerializableException` for `StringFormatValidation` in argument captor... What am I missing? Can you be more specific? **P.S.** please use @username when replying to comments so the user gets notified. – Morfic Mar 20 '19 at 15:50
  • @Morfic the assert statement does a contains check on `actualValidationList` for `testFormatValidation`. This is just an equals on `testFormatValidation` against everything present in `actualValidationList`. The equals for `StringFormatValidation` is written in terms of deserialization of the `StringFormatValidator`. This fails for desiarilizing the instance of `StringFormatValidator` which is present in `actualValidationList` which was populated using argument captor. Note that the serialization of `testFormatValidation` which is created in the test itself, using the same lambda, works fine. – nishantv Mar 21 '19 at 14:10

1 Answers1

0

After spending some time I found the problem and I would answer my own question so that it could help someone in a similar situation. I got insights from the following SO posts : 1. What is the difference between a lambda and a method reference at a runtime level? 2. Equality of instance of functional interface in java There were two problems that I encountered. First, the original problem as mentioned in the question above of java.io.NotSerializableException: Non-serializable lambda. I was under the impression that the argument capture from Mockito somehow interferes and the lambda argument captured is not serializable anymore. However it was more to do with how serialization of lambda in java happens in general. I still do not understand the internals fully but the exception was resolved in one of those situations when you really don't know what worked. I then encountered that the equals was failing because the serialized values contain the call site. Hence StringFormatValidation testFormatValidation1 = new StringFormatValidation(StringFormatValidators::isCommaSeparated,"test1"); created in test class will have the path of test class while same construct in the main class will have it's path. I solved this by extracting the StringFormatValidators::isCommaSeparated in a static variable and using that from all call sites.

 public class StringFormatValidators {

    private static boolean isCommaSeparatedFn(String arg) {
       String COMMA_SEPARATED_STRINGS = "^[a-zA-Z0-9]+[a-zA-Z0-9-_:]*(,[a-zA-Z0-9]+ 
        [a-zA-Z0-9-_:]*)*$";
       Pattern COMMA_SEPARATED_STRINGS_PATTERN = 
       Pattern.compile(COMMA_SEPARATED_STRINGS);
       return arg != null && COMMA_SEPARATED_STRINGS_PATTERN.matcher(arg).find();
    }

    public static final StringFormatValidator isCommaSeparated = 
       StringFormatValidators::isCommaSeparatedFn;
 }
nishantv
  • 643
  • 4
  • 9
  • 27