Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure mocks for annotation interfaces with default values from annotation #1900

Open
ghenzler opened this issue Apr 7, 2020 · 7 comments · May be fixed by #1901
Open

Configure mocks for annotation interfaces with default values from annotation #1900

ghenzler opened this issue Apr 7, 2020 · 7 comments · May be fixed by #1901

Comments

@ghenzler
Copy link

ghenzler commented Apr 7, 2020

Mockito.mock() can quite conveniently be used to not only create instances of interfaces, but also to create instances of annotation interfaces. Although normally those instances are created by using the annotations in source code (and subsequently using class.getAnnotations(), often in framework code) there are some use cases where the annotation instances are created by a framework - in particular this is the case for the OSGi Metatype Service (see https://osgi.org/specification/osgi.cmpn/7.0.0/service.metatype.html#d0e17824 for details). For this use case, a mock for an annotation interface should be automatically set up to return the annotation-defined default values.

The following JUnit Tests illustrates what is needed:

public @interface AnnotationInterface {
    String strAttWithDefault() default "default";
    String strAttWithoutDefault();
    String[] strArrAtt() default { "default1", "default2" };
    int intAtt() default 1;
    long longAtt() default 2L;
}

@Mock
AnnotationInterface mockedAnnotationInterface;

@Test
public void shouldMockDefaultValuesFromAnnoationInterface() throws Exception {
    // no further action should be needed to have the default values returned
    assertEquals("default", mockedAnnotationInterface.strAttWithDefault());
    assertNull(mockedAnnotationInterface.strAttWithoutDefault());
    assertArrayEquals(new String[] { "default1", "default2" }, mockedAnnotationInterface.strArrAtt());
    assertArrayEquals(new String[] { "default1", "default2" }, mockedAnnotationInterface.strArrAtt());
    assertEquals(1, mockedAnnotationInterface.intAtt());
    assertEquals(2L, mockedAnnotationInterface.longAtt());
}
ghenzler added a commit to ghenzler/mockito that referenced this issue Apr 7, 2020
ghenzler added a commit to ghenzler/mockito that referenced this issue Apr 7, 2020
@TimvdLippe
Copy link
Contributor

Intriguing feature request! Could you elaborate on a concrete example of a test where you would use this? I would assume that any annotation you specify is static and therefore should not be changed. E.g. you should use the real annotation instead of a mock. Would like to know more about that.

@ghenzler
Copy link
Author

ghenzler commented Apr 8, 2020

See https://github.com/apache/felix-dev/blob/f51a4a870364db5909bae8704548d028c0945d8b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/monitor/HealthCheckMonitorTest.java#L108 for where I added this functionality manually to a concrete test (this JUnit test was the trigger to start the discussion here).

In general, everybody developing with the OSGi Metatype Service (see https://osgi.org/specification/osgi.cmpn/7.0.0/service.metatype.html#d0e17824) is using OSGi framework-provided annotation instances to activate components. This may sound unusual, but the OSGi spec chose annotations for this use case because it very well fits to the semantics of what is needed (null not possible, defaults can be given in code, immutable). And as said, for the case @Mock is used with such an annotation it works already perfectly fine, the pain point is just that the defaults have to be manually set. Doing it automatically is a lot closer to reality - because in reality the properties can exactly never return null whereas a @Mock-created annotation instance does exactly that, return null by default.

Here are some examples from elsewhere to illustrate how those annotations are used with the OSGi Metatype Service:

Code: https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/bc9bda13b6e3cfd5aef6a13cb54b63ede213b06a/bundle/src/main/java/com/adobe/acs/commons/httpcache/config/impl/RequestCookieHttpCacheConfigExtension.java#L69-L87
The unit test uses an anonymous class (not so nice): https://github.com/Adobe-Consulting-Services/acs-aem-commons/blob/bc9bda13b6e3cfd5aef6a13cb54b63ede213b06a/bundle/src/test/java/com/adobe/acs/commons/httpcache/config/impl/RequestHeaderHttpCacheConfigExtensionTest.java#L43-L68

Or code: https://github.com/apache/sling-org-apache-sling-scripting-core/blob/a9f1b82bbf128cca76cf7ba6cba227d5a282d1f9/src/main/java/org/apache/sling/scripting/core/impl/ScriptCacheImplConfiguration.java#L30-L45 (no JUnit test at all for this, maybe because it is hard to mock it :) )

Overall, see https://github.com/search?q=org.osgi.service.metatype.ObjectClassDefinition+language%3Ajava&type=Code for a github search on where this is used (and this is github only). So I don't think it's too much of an exotic use case, it would be really valuable to have this in Mockito core.

@TimvdLippe
Copy link
Contributor

I understand the desire to fully test the annotations. However, my understanding of annotations is that they should always be attached to a symbol. E.g. they should be attached to a variable or field.

Therefore, I don't think allowing instances of annotations to live separate from the symbol they should be attached to is desirable. Instead, using the annotation directly on a Mock can help. Mockito makes sure that any annotations that are attached are preserved on runtime and are present on mocks.

Would that work for you?

@ghenzler
Copy link
Author

ghenzler commented Apr 10, 2020

Therefore, I don't think allowing instances of annotations to live separate from the symbol they should be attached to is desirable.

Fact is: OSGi Metatype Service (see https://osgi.org/specification/osgi.cmpn/7.0.0/service.metatype.html#d0e17824 ) makes use of annotations without ever attaching it to a symbol (not in declaration, not in usage). Java as language chose to implement annotations as a special type of interface (while still having all properties of an interface + some extra traits => immutability, default values, etc.). OSGi chose to use those traits without actually using the annotation part itself.

Now, to accommodate the nature of the annotation implementation in java, this PR makes perfectly sense.

using the annotation directly on a Mock can help

when using annotations directly on a symbol, you create immutable objects. To test all value combinations of an annotation this quickly becomes tedious because all value combinations have to be explicitly listed:

Given the annotation

@interface MyAnnotation {
        boolean boolProp() default true;
        boolean stringProp() default "val";
}

Using the annotation in a test connecting it to a symbol is cumbersome:

@MyAnnotation
Object holder1;
@MyAnnotation(boolProp: true)
Object holder2;
@MyAnnotation(stringProp: "differentVal")
Object holder3;
@MyAnnotation(boolProp: true, stringProp: "differentVal")
Object holder4;

@Test
public void testCombination1() {
   MyAnnotation annotationVal = getClass().getDeclaredField("holder1").getDeclaredAnnotation(MyAnnotation.class);
   classUnderTest.workWith(annotationVal);
   // no verification if an annotation getter is called or not is possible
}

@Test
public void testCombination2() {
   MyAnnotation annotationVal = getClass().getDeclaredField("holder2").getDeclaredAnnotation(MyAnnotation.class);
   classUnderTest.workWith(annotationVal);
   // no verification if an annotation getter is called or not is possible
}

@Test
public void testCombination3() {
   MyAnnotation annotationVal = getClass().getDeclaredField("holder3").getDeclaredAnnotation(MyAnnotation.class);
   classUnderTest.workWith(annotationVal);
   // no verification if an annotation getter is called or not is possible
}

@Test
public void testCombination4() {
   MyAnnotation annotationVal = getClass().getDeclaredField("holder4").getDeclaredAnnotation(MyAnnotation.class);
   classUnderTest.workWith(annotationVal);
   // no verification if an annotation getter is called or not is possible
}

By using a mock for this, the code becomes a lot cleaner:

@Mock
MyAnnotation myAnnotation;

// this issue is about getting rid of this, because return the correct default
// as given by the annotation always makes sense!
@Before
public void setup() {
    for (Method m : MyAnnotation.class.getDeclaredMethods()) {
        when(m.invoke(config)).thenReturn(m.getDefaultValue());
    }
}

@Test
public void testCombination1() {
   // defaults are in place!
   classUnderTest.workWith(myAnnotation);
   // verification if an annotation getter is called or not IS possible
}

@Test
public void testCombination2() {
   when(myAnnotation.boolProp()).thenReturn(true); // overwriting the default
   classUnderTest.workWith(myAnnotation);
   // verification if an annotation getter is called or not IS possible
}

@Test
public void testCombination3() {
   when(myAnnotation.stringProp()).thenReturn("differentVal");
   classUnderTest.workWith(myAnnotation);
   // verification if an annotation getter is called or not IS possible
}

@Test
public void testCombination4() {
   when(myAnnotation.boolProp()).thenReturn(true);
   when(myAnnotation.stringProp()).thenReturn("differentVal");
   classUnderTest.workWith(myAnnotation);
   // verification if an annotation getter is called or not IS possible
}

Now for this trivial example it might not even look that bad, but in OSGi metatype config annotations it can easily get to 10 member methods (many combinations if you cannot use when(..)... also as noted in the comments, verification cannot be done on annotations as retrieved from symbols because they are not mocks but immutable objects then.

@ghenzler
Copy link
Author

@TimvdLippe So I think think this PR would improve Mockito to be in line with the annotation implementation the java platform chose. Also because of the typeToMock.isAnnotation() check is quick, there is no performance impact to be expected for all other regular Mock usages. WDYT?

@exaucae
Copy link

exaucae commented May 20, 2021

Bumpl!

@tyge68
Copy link

tyge68 commented Mar 30, 2023

👍 I have the same issue, it would be nice that mockito would detect and automatically mock those default, so that it would not look suspicious in the test code that it force the mock with when/ thenReturn..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants