如何从 JUnit 测试中的模拟对象获取 typeName()?

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

这是我不得不嘲笑的最奇怪的方法之一。我需要以某种方式协调我的单元测试与以下代码:

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

看起来它可能是一个简单的单元测试(我将 getBeanInfo() 移到了一个单独的方法中,这样我就可以模拟它而不会被 Introspector 绊倒)。但是,每当我到达 getTypeName() 时,它总是会抛出 InvocationTargetException。有没有办法以某种方式模拟 PropertyDescriptor 的 属性 类型?我在 Whosebug 上找到了一个解决方案,但没有太大帮助。

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

这是我如何模拟 BenInfo 对象的代码:

@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.
}

mockValue 对象只是一个通用对象。 mockBeanInfo 对象是不言自明的。此代码确实有效。问题是模拟 PropertyDescriptor 名称。

这里是 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;
}

最后是 mockBeanInfo:

@Mock private java.beans.BeanInfo mockBeanInfo;

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.

换句话说,Bean只是一种数据结构。它没有任何行为,也没有您想通过模拟防止发生的意外后果。换句话说,你根本不应该嘲笑 BeanInfo .

但是,您确实希望确保 class 对 BeanInfo 对象做正确的事情。您希望在 生产代码和测试 中都获得 real BeanInfo 对象,因为它是一种数据结构。因此,您真正需要的是一种在测试方法中访问这些真实 BeanInfo 对象的方法。

注意:您将无法避免在此处使用真正的 Introspector,因为您的应用程序需要它提供的数据。

以下是我将如何解决您的问题:

  1. 重构您的 getBeanInfo() 行为以使用单独的 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. 可能通过添加构造函数参数将此行为注入 ClientViewer

    private final BeanInfoProvider provider;
    
    public ClientViewer(..., BeanInfoProvider provider) {
      // snip
      this.provider = provider;
    }  
    
  3. 将使用 BeanInfo 的方法更改为使用此 BeanInfoProvider

    protected void sub(Object obj) {
      try {
        BeanInfo beanInfo = provider.getBeanInfo(obj);
        // snip
    
  4. 实现 BeanInfoProvider 生成 spies 并允许您访问它们。注意:您需要缓存 BeanInfo 间谍以确保您在 ClientViewer 和您的测试方法中获得相同的间谍。

    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. 将其注入到您的测试中

    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.
    }
    

这将允许您访问生成的 BeanInfo 对象 - 因为它们是真实的数据结构,实现为部分模拟,您将不会再获得这些 InvocationTargetException .