【问题标题】:Using Mockito to test abstract classes使用 Mockito 测试抽象类
【发布时间】:2010-11-08 09:50:37
【问题描述】:

我想测试一个抽象类。当然,我可以 manually write a mock 继承自该类。

我可以使用模拟框架(我正在使用 Mockito)而不是手工制作我的模拟来做到这一点吗?怎么样?

【问题讨论】:

  • 从 Mockito 1.10.12 开始,Mockito 支持直接监视/模拟抽象类:SomeAbstract spy = spy(SomeAbstract.class);
  • 从 Mockito 2.7.14 开始,您还可以通过 mock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS)) 模拟需要构造函数参数的抽象类

标签: java unit-testing mocking abstract-class mockito


【解决方案1】:

以下建议让您在不创建“真实”子类的情况下测试抽象类 - Mock 子类。

使用Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS),然后模拟任何被调用的抽象方法。

例子:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

注意:此解决方案的美妙之处在于您不必实现抽象方法,只要它们从未被调用。

老实说,这比使用 spy 更简洁,因为 spy 需要一个实例,这意味着您必须创建抽象类的可实例化子类。

【讨论】:

  • 如下所述,当抽象类调用抽象方法进行测试时,这不起作用,这种情况经常发生。
  • 这在抽象类调用抽象方法时确实有效。只需使用 doReturn 或 doNothing 语法而不是 Mockito.when 来存根抽象方法,如果您存根任何具体调用,请确保先存根抽象调用。
  • 如何在这种对象中注入依赖项(模拟抽象类调用真实方法)?
  • 如果有问题的类有实例初始化器,这会以意想不到的方式表现。 Mockito 跳过模拟的初始化程序,这意味着内联初始化的实例变量将意外地为空,这可能导致 NPE。
  • 抽象类构造函数带一个或多个参数怎么办?
【解决方案2】:

如果你只需要测试一些具体方法而不接触任何抽象,你可以使用CALLS_REAL_METHODS(见Morten's answer),但是如果被测试的具体方法调用了一些抽象,或者未实现的接口方法,这是行不通的——Mockito 会抱怨“无法在 Java 接口上调用真实方法。”

(是的,这是一个糟糕的设计,但是一些框架,例如 Tapestry 4,有点强加给你。)

解决方法是颠倒这种方法——使用普通的模拟行为(即,一切都被模拟/存根)并使用doCallRealMethod() 显式调用测试中的具体方法。例如

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

更新添加:

对于非 void 方法,您需要改用 thenCallRealMethod(),例如:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

否则 Mockito 会报错“检测到未完成的存根。”

【讨论】:

  • 这在某些情况下会起作用,但是 Mockito 不会使用此方法调用底层抽象类的构造函数。这可能会导致“真实方法”由于创建了意外场景而失败。因此,此方法也并非在所有情况下都有效。
  • 是的,你根本不能指望对象的状态,只能指望被调用方法中的代码。
  • 哦,所以对象方法从状态中分离出来了,太好了。
【解决方案3】:

您可以通过使用间谍来实现这一点(尽管使用最新版本的 Mockito 1.8+)。

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));

【讨论】:

    【解决方案4】:

    Mocking 框架旨在更轻松地模拟您正在测试的类的依赖项。当您使用模拟框架模拟一个类时,大多数框架会动态创建一个子类,并将方法实现替换为用于检测何时调用方法并返回假值的代码。

    在测试抽象类时,您希望执行被测对象 (SUT) 的非抽象方法,因此模拟框架不是您想要的。

    部分困惑是,您链接到的问题的答案是手工制作一个从您的抽象类扩展的模拟。我不会把这样的课程称为模拟。模拟是用作替代依赖项的类,根据期望进行编程,并且可以查询以查看是否满足这些期望。

    相反,我建议在测试中定义抽象类的非抽象子类。如果这导致代码过多,则可能表明您的类难以扩展。

    另一种解决方案是使您的测试用例本身抽象化,并使用一种抽象方法来创建 SUT(换句话说,测试用例将使用Template Method 设计模式)。

    【讨论】:

      【解决方案5】:

      尝试使用自定义答案。

      例如:

      import org.mockito.Mockito;
      import org.mockito.invocation.InvocationOnMock;
      import org.mockito.stubbing.Answer;
      
      public class CustomAnswer implements Answer<Object> {
      
          public Object answer(InvocationOnMock invocation) throws Throwable {
      
              Answer<Object> answer = null;
      
              if (isAbstract(invocation.getMethod().getModifiers())) {
      
                  answer = Mockito.RETURNS_DEFAULTS;
      
              } else {
      
                  answer = Mockito.CALLS_REAL_METHODS;
              }
      
              return answer.answer(invocation);
          }
      }
      

      它将返回抽象方法的模拟,并调用具体方法的真实方法。

      【讨论】:

        【解决方案6】:

        真正让我对模拟抽象类感到难过的是这样一个事实,即既没有调用默认构造函数 YourAbstractClass()(在模拟中缺少 super()),也没有在 Mockito 中默认初始化模拟属性(例如List 属性为空ArrayListLinkedList)。

        我的抽象类(基本上是生成类源代码)没有为列表元素提供依赖项设置器注入,也没有提供初始化列表元素的构造函数(我尝试手动添加)。

        只有类属性使用默认初始化:

        private List<MyGenType> dep1 = new ArrayList<MyGenType>();
        private List<MyGenType> dep2 = new ArrayList<MyGenType>();
        

        因此,如果不使用真实的对象实现(例如,单元测试类中的内部类定义、重写抽象方法)和窥探真实对象(进行正确的字段初始化),就无法模拟抽象类。

        太糟糕了,只有PowerMock 会在这里提供更多帮助。

        【讨论】:

          【解决方案7】:

          假设您的测试类与您的测试类在同一个包中(在不同的源根目录下),您可以简单地创建模拟:

          YourClass yourObject = mock(YourClass.class);
          

          并像调用任何其他方法一样调用要测试的方法。

          您需要为调用的每个方法提供期望,并期望调用超级方法的任何具体方法 - 不确定如何使用 Mockito 做到这一点,但我相信使用 EasyMock 是可能的。

          这一切所做的只是创建YouClass 的具体实例,并为您节省提供每个抽象方法的空实现的工作。

          顺便说一句,我经常发现在我的测试中实现抽象类很有用,它作为我通过其公共接口测试的示例实现,尽管这确实取决于抽象类提供的功能。

          【讨论】:

          • 但是使用mock并不会测试YourClass的具体方法,还是我错了?这不是我想要的。
          • 没错,如果你想调用抽象类的具体方法,上面的就不行了。
          • 抱歉,我将编辑关于期望的部分,您调用的每个方法都需要这些期望,而不仅仅是抽象方法。
          • 但是你仍然在测试你的模拟,而不是具体的方法。
          【解决方案8】:

          您可以在测试中使用匿名类扩展抽象类。 例如(使用 Junit 4):

          private AbstractClassName classToTest;
          
          @Before
          public void preTestSetup()
          {
              classToTest = new AbstractClassName() { };
          }
          
          // Test the AbstractClassName methods.
          

          【讨论】:

            【解决方案9】:

            Mockito 允许通过 @Mock 注释模拟抽象类:

            public abstract class My {
            
                public abstract boolean myAbstractMethod();
            
                public void myNonAbstractMethod() {
                    // ...
                }
            }
            
            @RunWith(MockitoJUnitRunner.class)
            public class MyTest {
            
                @Mock(answer = Answers.CALLS_REAL_METHODS)
                private My my;
            
                @Test
                private void shouldPass() {
                    BDDMockito.given(my.myAbstractMethod()).willReturn(true);
                    my.myNonAbstractMethod();
                    // ...
                }
            }
            

            缺点是需要构造函数参数时不能使用。

            【讨论】:

              【解决方案10】:
              class Dependency{
                public void method(){};
              }
              
              public abstract class My {
              
                private Dependency dependency;
                public abstract boolean myAbstractMethod();
              
                public void myNonAbstractMethod() {
                  // ...
                  dependency.method();
                }
              }
              
              @RunWith(MockitoJUnitRunner.class)
              public class MyTest {
              
                @InjectMocks
                private My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
                // we can mock dependencies also here
                @Mock
                private Dependency dependency;
              
                @Test
                private void shouldPass() {
                  // can be mock the dependency object here.
                  // It will be useful to test non abstract method
                  my.myNonAbstractMethod();
                }
              }
              

              【讨论】:

              • 这正是我所需要的——使用@InjectMocks 对抽象类进行测试。感谢您添加此答案!
              【解决方案11】:

              您可以实例化一个匿名类,注入您的模拟,然后测试该类。

              @RunWith(MockitoJUnitRunner.class)
              public class ClassUnderTest_Test {
              
                  private ClassUnderTest classUnderTest;
              
                  @Mock
                  MyDependencyService myDependencyService;
              
                  @Before
                  public void setUp() throws Exception {
                      this.classUnderTest = getInstance();
                  }
              
                  private ClassUnderTest getInstance() {
                      return new ClassUnderTest() {
              
                          private ClassUnderTest init(
                                  MyDependencyService myDependencyService
                          ) {
                              this.myDependencyService = myDependencyService;
                              return this;
                          }
              
                          @Override
                          protected void myMethodToTest() {
                              return super.myMethodToTest();
                          }
                      }.init(myDependencyService);
                  }
              }
              

              请记住,抽象类ClassUnderTest 的属性myDependencyService 的可见性必须为protected

              【讨论】:

                【解决方案12】:

                PowerMock 的Whitebox.invokeMethod(..) 在这种情况下可以派上用场。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2014-01-02
                  • 1970-01-01
                  • 2010-09-16
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-07-24
                  相关资源
                  最近更新 更多