使用 Mockito 测试抽象类

发布于 2024-07-26 09:30:29 字数 203 浏览 7 评论 0 原文

我想测试一个抽象类。 当然,我可以手动编写一个继承自该类的模拟

我可以使用模拟框架(我正在使用 Mockito)来完成此操作,而不是手工制作我的模拟吗? 如何?

I'd like to test an abstract class. Sure, I can manually write a mock that inherits from the class.

Can I do this using a mocking framework (I'm using Mockito) instead of hand-crafting my mock? How?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

温柔嚣张 2024-08-02 09:30:30

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

@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 的可见性必须受到保护

You can instantiate an anonymous class, inject your mocks and then test that class.

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

Keep in mind that the visibility must be protected for the property myDependencyService of the abstract class ClassUnderTest.

吹梦到西洲 2024-08-02 09:30:30

PowerMock 的 Whitebox.invokeMethod(..) 在这种情况下会很方便。

PowerMock's Whitebox.invokeMethod(..) can be handy in this case.

苏佲洛 2024-08-02 09:30:29

以下建议允许您测试抽象类,而无需创建“真正的”子类 - 模拟子类,并且只是部分模拟。

使用 Mockito.mock(My.class, Answers.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, Answers.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

注意:此解决方案的优点在于您不必实现抽象方法。 CALLS_REAL_METHODS 会导致所有实际方法按原样运行,只要您不在测试中对它们进行存根即可。

在我看来,这比使用间谍更简洁,因为间谍需要一个实例,这意味着您必须创建抽象类的可实例化子类。

The following suggestion lets you test abstract classes without creating a "real" subclass - the Mock is the subclass and only a partial mock.

Use Mockito.mock(My.class, Answers.CALLS_REAL_METHODS), then mock any abstract methods that are invoked.

Example:

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

Note: The beauty of this solution is that you do not have to implement the abstract methods. CALLS_REAL_METHODS causes all real methods to be run as is, as long as you don't stub them in your test.

In my honest opinion, this is neater than using a spy, since a spy requires an instance, which means you have to create an instantiable subclass of your abstract class.

云淡风轻 2024-08-02 09:30:29

如果您只需要测试一些具体方法而不涉及任何抽象,则可以使用 CALLS_REAL_METHODS (请参阅Morten 的回答),但是如果被测试的具体方法调用一些抽象方法或未实现的接口方法,则这将不起作用 - 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 会抱怨“检测到未完成的存根”。

If you just need to test some of the concrete methods without touching any of the abstracts, you can use CALLS_REAL_METHODS (see Morten's answer), but if the concrete method under test calls some of the abstracts, or unimplemented interface methods, this won't work -- Mockito will complain "Cannot call real method on java interface."

(Yes, it's a lousy design, but some frameworks, e.g. Tapestry 4, kind of force it on you.)

The workaround is to reverse this approach -- use the ordinary mock behavior (i.e., everything's mocked/stubbed) and use doCallRealMethod() to explicitly call out the concrete method under test. E.g.

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

Updated to add:

For non-void methods, you'll need to use thenCallRealMethod() instead, e.g.:

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

Otherwise Mockito will complain "Unfinished stubbing detected."

葬﹪忆之殇 2024-08-02 09:30:29

您可以通过使用间谍来实现这一点(不过请使用最新版本的 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()));

You can achieve this by using a spy (use the latest version of Mockito 1.8+ though).

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()));
一萌ing 2024-08-02 09:30:29

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

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

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

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

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

Mocking frameworks are designed to make it easier to mock out dependencies of the class you are testing. When you use a mocking framework to mock a class, most frameworks dynamically create a subclass, and replace the method implementation with code for detecting when a method is called and returning a fake value.

When testing an abstract class, you want to execute the non-abstract methods of the Subject Under Test (SUT), so a mocking framework isn't what you want.

Part of the confusion is that the answer to the question you linked to said to hand-craft a mock that extends from your abstract class. I wouldn't call such a class a mock. A mock is a class that is used as a replacement for a dependency, is programmed with expectations, and can be queried to see if those expectations are met.

Instead, I suggest defining a non-abstract subclass of your abstract class in your test. If that results in too much code, than that may be a sign that your class is difficult to extend.

An alternative solution would be to make your test case itself abstract, with an abstract method for creating the SUT (in other words, the test case would use the Template Method design pattern).

巨坚强 2024-08-02 09:30:29

尝试使用自定义答案。

例如:

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

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

Try using a custom answer.

For example:

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

It will return the mock for abstract methods and will call the real method for concrete methods.

自演自醉 2024-08-02 09:30:29
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();
  }
}
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();
  }
}
菩提树下叶撕阳。 2024-08-02 09:30:29

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

缺点是如果需要构造函数参数,则无法使用它。

Mockito allows mocking abstract classes by means of the @Mock annotation:

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

The disadvantage is that it cannot be used if you need constructor parameters.

哀由 2024-08-02 09:30:29

真正让我对模拟抽象类感到难过的是,默认构造函数 YourAbstractClass() 都没有被调用(模拟中缺少 super()),似乎也没有调用。可以在 Mockito 中以任何方式默认初始化模拟属性(例如带有空 ArrayListLinkedListList 属性)。

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

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

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

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

太糟糕了,只有 PowerMock 可以进一步提供帮助。

What really makes me feel bad about mocking abstract classes is the fact, that neither the default constructor YourAbstractClass() gets called (missing super() in mock) nor seems there to be any way in Mockito to default initialize mock properties (e.g List properties with empty ArrayList or LinkedList).

My abstract class (basically the class source code gets generated) does NOT provide a dependency setter injection for list elements, nor a constructor where it initializes the list elements (which I tried to add manually).

Only the class attributes use default initialization:

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

So there is NO way to mock an abstract class without using a real object implementation (e.g inner class definition in unit test class, overriding abstract methods) and spying the real object (which does proper field initialization).

Too bad that only PowerMock would help here further.

甩你一脸翔 2024-08-02 09:30:29

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

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.

You can extend the abstract class with an anonymous class in your test.
For example (using Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.
箹锭⒈辈孓 2024-08-02 09:30:29

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

YourClass yourObject = mock(YourClass.class);

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

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

所有这一切都是为了创建 YouClass 的具体实例,并节省您提供每个抽象方法的空实现的精力。

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

Assuming your test classes are in the same package (under a different source root) as your classes under test you can simply create the mock:

YourClass yourObject = mock(YourClass.class);

and call the methods you want to test just as you would any other method.

You need to provide expectations for each method that is called with the expectation on any concrete methods calling the super method - not sure how you'd do that with Mockito, but I believe it's possible with EasyMock.

All this is doing is creating a concrete instance of YouClass and saving you the effort of providing empty implementations of each abstract method.

As an aside, I often find it useful to implement the abstract class in my test, where it serves as an example implementation that I test via its public interface, although this does depend on the functionality provided by the abstract class.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文