javajunitmockingmockitopropertydescriptor

How do I get typeName() from a mock object in a JUnit test?


This is one of the strangest methods I've had to mock. I need to somehow reconcile my unit test with the following code:

protected void sub(Object obj) {
    try {
        BeanInfo beanInfo = getBeanInfo(obj);
        for (PropertyDescriptor pb : beanInfo.getPropertyDescriptors()) {
            String fieldType = pd.getPropertyType.getTypeName();
            System.out.println(fieldType);
        }
    } catch (InvocationTargetException | IllegalAccessException e) {
        e.printStackTrace();
    }
}

It LOOKS like it might be a straightforward unit test (I moved getBeanInfo() to a separate method so I could mock it without tripping over Introspector). However, it will always throw an InvocationTargetException whenever I get to getTypeName(). Is there a way to somehow mock a PropertyDescriptor's property type? I found a solution here on stackoverflow, but it hasn't helped much.

A strange generics edge case with Mockito.when() and generic type inference

Here's the code for how I mock the BenInfo object:

@Test
public void testSub() {
    ClientViewer cv = mock(ClientViewer.class); // The class that I'm testing.
    when(cv.getBeanInfo(mockValue)).thenReturn(mockBeanInfo);

    // Rest of the test.
}

The mockValue object is just a generic object. The mockBeanInfo object is pretty self-explanatory. This code does work. The problem is mocking the PropertyDescriptor name.

Here's getBeanInfo():

protected BeanInfo getBeanInfo(Object obj) {
    BeanInfo beanInfo = null;

    try {
        Class cls = obj.getClas();
        beanInfo = Introspector.getBeanInfo(cls);
    } catch (IntrospectionException e) {
        e.printStackTrace();
    }

    return beanInfo;
}

And finally mockBeanInfo:

@Mock private java.beans.BeanInfo mockBeanInfo;

Solution

  • Let's talk about what a Java Bean is:

    1. All properties private (use getters/setters)
    2. A public no-argument constructor
    3. Implements Serializable.

    In other words, a Bean is just a data structure. It has no behavior, no unintended consequences that you want to prevent from happening with a mock. In other words, you shouldn't be mocking BeanInfo at all.

    However, you do want to ensure that your class is doing the right things with your BeanInfo objects. You want to get real BeanInfo objects in both your production code and your test, because it's a data structure. So, what you really need is a way to get access to these real BeanInfo objects in your test method.

    Note: You're not going to be able to avoid using the real Introspector here, because your application needs the data it provides.

    Here's how I would fix your problem:

    1. Refactor your getBeanInfo() behavior to use a separate class, BeanInfoProvider:

      public class SimpleBeanInfoProvider implements BeanInfoProvider {
        public BeanInfo getBeanInfo(Object obj) {
          BeanInfo beanInfo = null;
      
          try {
            Class cls = obj.getClass();
            beanInfo = Introspector.getBeanInfo(cls);
          } catch (IntrospectionException e) {
            e.printStackTrace();
          }
      
          return beanInfo;
        }
      }
      
    2. Inject this behavior into ClientViewer, probably by adding a constructor argument.

      private final BeanInfoProvider provider;
      
      public ClientViewer(..., BeanInfoProvider provider) {
        // snip
        this.provider = provider;
      }  
      
    3. Change your methods that use BeanInfo to use this BeanInfoProvider

      protected void sub(Object obj) {
        try {
          BeanInfo beanInfo = provider.getBeanInfo(obj);
          // snip
      
    4. Make an implementation of BeanInfoProvider that generates spies and allows you to access them. Note: you need to cache the BeanInfo spy to make sure you get the same one inside ClientViewer and your test method.

      public class SpyBeanInfoProvider implements BeanInfoProvider {
        private final BeanInfoProvider delegate;
        private final Map<Class<?>, BeanInfo> spyMap = new HashMap<>(); 
      
        public SpyBeanInfoProvider(BeanInfoProvider delegate) {
          this.delegate = delegate;
        }
      
        @Override
        public BeanInfo getBeanInfo(Object obj) {
          Class<?> klass = obj.getClass();
          if(!spyMap.containsKey(klass)) {
            BeanInfo info = spy(delegate.getBeanInfo(obj));
            spyMap.put(klass, info);
            return info;
          } else {
            return spyMap.get(obj);
          }
        }
      }
      
    5. Inject this into your test

      private BeanInfoProvider makeBeanInfoProvider() {
        return new SpyBeanInfoProvider(new IntrospectorBeanInfoProvider());
      }
      
      @Test
      public void testSub() {
        BeanInfoProvider provider = makeBeanInfoProvider();
        ClientViewer viewer = new ClientViewer(makeBeanInfoProvider());
        viewer.sub(obj);
        BeanInfo spy = provider.getBeanInfo(obj);
      
        // Now do your test
        verify(spy).getPropertyDescriptors();
        // etc.
      }
      

    This will allow you to have access to the BeanInfo objects that get generated - because they are real data structures, implemented as partial mocks, and you won't get these InvocationTargetExceptions anymore.