Verify that all getter methods are called

2019-02-22 07:56发布

问题:

I have the following test where I need to verify that all getters of the Person class are being called. So far I have used mockito's verify() to make sure that each getter is called. Is there a way to do that by reflection? It can be the case that a new getter is added to the Person class but the test will miss that.

public class GetterTest {
    class Person{

        private String firstname;
        private String lastname;

        public String getFirstname() {
            return firstname;
        }

        public String getLastname() {
            return lastname;
        }
    }

    @Test
    public void testAllGettersCalled() throws IntrospectionException{
        Person personMock = mock(Person.class);
        personMock.getFirstname();
        personMock.getLastname();

        for(PropertyDescriptor property : Introspector.getBeanInfo(Person.class).getPropertyDescriptors()) {
            verify(personMock, atLeast(1)).getFirstname();
            //**How to verify against any getter method and not just getFirstName()???**
        }
    }
}

回答1:

Generally, don't mock the class under test. If your test is for a Person, you shouldn't ever see Mockito.mock(Person.class) in it, as that's a pretty clear sign that you're testing the mocking framework instead of the system-under-test.

Instead, you may want to create a spy(new Person()), which will create a real Person implementation using a real constructor and then copy its data to a Mockito-generated proxy. You can use MockingDetails.getInvocations() to reflectively check that every getter was called.

// This code is untested, but should get the point across. Edits welcome.
// 2016-01-20: Integrated feedback from Georgios Stathis. Thanks Georgios!

@Test
public void callAllGetters() throws Exception {
  Person personSpy = spy(new Person());
  personSpy.getFirstname();
  personSpy.getLastname();

  assertAllGettersCalled(personSpy, Person.class);
}

private static void assertAllGettersCalled(Object spy, Class<?> clazz) {
  BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
  Set<Method> setOfDescriptors = beanInfo.getPropertyDescriptors()
      .stream()
      .map(PropertyDescriptor::getReadMethod)
      .filter(p -> !p.getName().contains("getClass"))
      .collect(Collectors.toSet());
  MockingDetails details = Mockito.mockingDetails(spy);
  Set<Method> setOfTestedMethods = details.getInvocations()
      .stream()
      .map(InvocationOnMock::getMethod)
      .collect(Collectors.toSet());
  setOfDescriptors.removeAll(setOfTestedMethods);
  // The only remaining descriptors are untested.
  assertThat(setOfDescriptors).isEmpty();
}

There might be a way to call verify and invoke on the Mockito-generated spy, but that seems very fragile, and very dependent on Mockito internals.

As an aside, testing bean-style getters seems like an odd use of time/effort. In general focus on testing implementations that are likely to change or break.



回答2:

I can think of two solutions for your problem:

  1. Generate the Builder code programmatically, so you don't need to run tests. Java code is generated by a program and never edited by a user. Test the generator instead. Use a text template and build definitions from a serialized domain model or directly from Java compiled classes (you'll need a separate module dependent on the bean's one)

  2. Write your tests against a proxy library. The problem is that regular proxies can only implement interfaces, not regular classes, and it's very cumbersome to have interfaces for Javabeans. If you choose this route, I'd go with Javassist. I coded a runnable sample and put it on GitHub. The test cases use a proxy factory to instantiate beans (instead of using new)

public class CountingCallsProxyFactory {

    public <T> T proxy(Class<T> classToProxy) {
        ProxyFactory factory = new ProxyFactory();
        factory.setSuperclass(classToProxy);
        Class clazz = factory.createClass();
        T instance =  (T) clazz.newInstance();
        ProxyObject proxy = (ProxyObject) instance;
        MethodCallCounter handler = new MethodCallCounter();
        proxy.setHandler(handler);
        return instance;
    }

    public void verifyAllGettersCalled(Object bean) {
        // Query the counter against the properties in the bean
    }
}

The counter is kept inside the class MethodCallCounter