初始化模拟对象 - MockIto

Posted

技术标签:

【中文标题】初始化模拟对象 - MockIto【英文标题】:Initialising mock objects - MockIto 【发布时间】:2013-03-07 20:35:30 【问题描述】:

有很多方法可以使用 MockIto 初始化模拟对象。 其中最好的方法是什么?

1.

 public class SampleBaseTestCase 

   @Before public void initMocks() 
       MockitoAnnotations.initMocks(this);
   
@RunWith(MockitoJUnitRunner.class)
mock(XXX.class);

如果有比这些更好的方法,请建议我...

【问题讨论】:

【参考方案1】:

1.使用 MockitoAnnotations.openMocks()

Mockito 2 中的 MockitoAnnotations.initMock() 方法已弃用,并在 Mockito 3 中替换为 MockitoAnnotations.openMocks()MockitoAnnotations.openMocks() 方法返回一个 AutoClosable 实例,可用于在测试后关闭资源。下面是使用MockitoAnnotations.openMocks() 的示例。

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;


class MyTestClass 

    AutoCloseable openMocks;

    @BeforeEach
    void setUp() 
        openMocks = MockitoAnnotations.openMocks(this);
        // my setup code...
    

    @Test
    void myTest() 
        // my test code...
        
    

    @AfterEach
    void tearDown() throws Exception 
        // my tear down code...
        openMocks.close();
    


2。使用@ExtendWith(MockitoExtension.class):

从 JUnit5 开始,@RunWith 已被删除。下面是一个使用@ExtendWith的例子:

@ExtendWith(MockitoExtension.class)
class MyTestClass 

    @BeforeEach
    void setUp() 
        // my setup code...
    

    @Test
    void myTest() 
        // my test code...

    

    @AfterEach
    void tearDown() throws Exception 
        // my tear down code...
    


【讨论】:

【参考方案2】:

Mockito 的最新版本中,方法MockitoAnnotations.initMocks 已弃用

首选方式是使用

MockitoJUnitRunnerMockitoRuleJUnit4 MockitoExtensionJUnit5 MockitoTestNGListenerTestNG

如果你不能使用专用的跑步者/扩展,你可以使用MockitoSession

【讨论】:

【参考方案3】:

对于 mocks 初始化,使用 runner 或 MockitoAnnotations.initMocks 是严格等效的解决方案。来自MockitoJUnitRunner 的javadoc:

JUnit 4.5 运行器初始化使用 Mock 注释的模拟,因此不需要显式使用 MockitoAnnotations.initMocks(Object)。模拟在每个测试方法之前初始化。


当您已经在测试用例上配置了特定的运行器(例如SpringJUnit4ClassRunner)时,可以使用第一个解决方案(使用MockitoAnnotations.initMocks)。

第二种解决方案(使用MockitoJUnitRunner)更经典,也是我最喜欢的。代码更简单。使用跑步者提供了 automatic validation of framework usage 的巨大优势(由@David Wallace 在this answer 中描述)。

两种解决方案都允许在测试方法之间共享模拟(和间谍)。再加上@InjectMocks,它们允许非常快速地编写单元测试。样板模拟代码减少了,测试更容易阅读。例如:

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest 

    @Mock private ArticleCalculator calculator;
    @Mock(name = "database") private ArticleDatabase dbMock;
    @Spy private UserProvider userProvider = new ConsumerUserProvider();

    @InjectMocks private ArticleManager manager;

    @Test public void shouldDoSomething() 
        manager.initiateArticle();
        verify(database).addListener(any(ArticleListener.class));
    

    @Test public void shouldDoSomethingElse() 
        manager.finishArticle();
        verify(database).removeListener(any(ArticleListener.class));
    

优点:代码最少

缺点:黑魔法。 IMO 这主要是由于 @InjectMocks 注释。有了这个注解“你摆脱了代码的痛苦”(参见 @Brice 的伟大 cmets)


第三种解决方案是在每个测试方法上创建模拟。 正如@mlk 在其回答中所解释的那样,它允许“self-tained test”。

public class ArticleManagerTest 

    @Test public void shouldDoSomething() 
        // given
        ArticleCalculator calculator = mock(ArticleCalculator.class);
        ArticleDatabase database = mock(ArticleDatabase.class);
        UserProvider userProvider = spy(new ConsumerUserProvider());
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.initiateArticle();

        // then 
        verify(database).addListener(any(ArticleListener.class));
    

    @Test public void shouldDoSomethingElse() 
        // given
        ArticleCalculator calculator = mock(ArticleCalculator.class);
        ArticleDatabase database = mock(ArticleDatabase.class);
        UserProvider userProvider = spy(new ConsumerUserProvider());
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.finishArticle();

        // then
        verify(database).removeListener(any(ArticleListener.class));
    

优点:您清楚地展示了您的 api 是如何工作的(BDD...)

缺点:有更多样板代码。 (模拟创作)


我的建议是妥协。将@Mock 注释与@RunWith(MockitoJUnitRunner.class) 一起使用,但不要使用@InjectMocks

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest 

    @Mock private ArticleCalculator calculator;
    @Mock private ArticleDatabase database;
    @Spy private UserProvider userProvider = new ConsumerUserProvider();

    @Test public void shouldDoSomething() 
        // given
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.initiateArticle();

        // then 
        verify(database).addListener(any(ArticleListener.class));
    

    @Test public void shouldDoSomethingElse() 
        // given
        ArticleManager manager = new ArticleManager(calculator, 
                                                    userProvider, 
                                                    database);

        // when 
        manager.finishArticle();

        // then 
        verify(database).removeListener(any(ArticleListener.class));
    

优点:您清楚地展示了您的 api 是如何工作的(我的 ArticleManager 是如何实例化的)。没有样板代码。

缺点:测试不是独立的,减少代码的痛苦

【讨论】:

请注意,注解很有用,但它们并不能保护您免受糟糕的 OO 设计(或降级)的影响。就个人而言,虽然我很乐意减少样板代码,但我摆脱了代码(或 PITA)的痛苦,这是将设计更改为更好的触发器,因此我和团队正在关注 OO 设计。我觉得遵循 SOLID 设计或 GOOS 理念等原则的 OO 设计比选择如何实例化 mock 更重要。 (跟进)如果您没有看到这个对象是如何创建的,您不会对此感到痛苦,如果应该添加新功能,未来的程序员可能不会反应良好。无论如何,这两种方式都有争议,我只是说要小心。 这两个等价是不正确的。更简单的代码是使用MockitoJUnitRunner 的唯一优势,这不是真的。有关差异的更多信息,请参阅***.com/questions/10806345/… 的问题和我的回答。 @Gontard 是的,依赖项是可见的,但我看到使用这种方法的代码出错了。关于使用Collaborator collab = mock(Collaborator.class),我认为这种方式肯定是一种有效的方法。虽然这可能会很冗长,但您可以获得测试的可理解性和可重构性。两种方式各有利弊,我还没有决定哪种方法更好。 Amyway 总是有可能写废话,并且可能取决于上下文和编码器。 @mlk 我完全同意你的看法。我的英语不是很好,而且缺乏细微差别。我的意思是坚持使用 UNIT 这个词。【参考方案4】:

JUnit 5 Jupiter 的一个小例子,“RunWith”已被删除,您现在需要使用“@ExtendWith”注释来使用扩展。

@ExtendWith(MockitoExtension.class)
class FooTest 

  @InjectMocks
  ClassUnderTest test = new ClassUnderTest();

  @Spy
  SomeInject bla = new SomeInject();

【讨论】:

【参考方案5】:

其他答案很棒,如果您需要/需要,可以包含更多详细信息。 除此之外,我想添加一个 TL;DR:

    喜欢使用 @RunWith(MockitoJUnitRunner.class) 如果你不能(因为你已经使用了不同的跑步者),宁愿使用 @Rule public MockitoRule rule = MockitoJUnit.rule(); 与 (2) 类似,但您应该不要再使用它: @Before public void initMocks() MockitoAnnotations.initMocks(this); 如果您只想在其中一个测试中使用模拟并且不想将其暴露给同一测试类中的其他测试,请使用 X x = mock(X.class)

(1) 和 (2) 和 (3) 是互斥的。 (4) 可与其他组合使用。

【讨论】:

【参考方案6】:

现在(从 v1.10.7 开始)有第四种方法来实例化模拟,它使用名为 MockitoRule 的 JUnit4 规则

@RunWith(JUnit4.class)   // or a different runner of your choice
public class YourTest
  @Rule public MockitoRule rule = MockitoJUnit.rule();
  @Mock public YourMock yourMock;

  @Test public void yourTestMethod()  /* ... */ 

JUnit 查找 subclasses of TestRule annotated with @Rule,并使用它们包装 Runner 提供的测试语句。这样做的结果是您可以将 @Before 方法、@After 方法,甚至 try...catch 包装器提取到规则中。您甚至可以在测试中与这些进行交互,就像 ExpectedException 所做的那样。

MockitoRule 的行为几乎与 MockitoJUnitRunner 完全一样,除了您可以使用任何其他运行器,例如 Parameterized(它允许您的测试构造函数接受参数,因此您的测试可以运行多次),或 Robolectric 的测试运行器(因此它的类加载器可以为 android 原生类提供 Java 替代品)。这使得它在最近的 JUnit 和 Mockito 版本中使用起来更加灵活。

总结:

Mockito.mock():直接调用,不支持注释或使用验证。 MockitoAnnotations.initMocks(this):注解支持,无使用验证。 MockitoJUnitRunner:注解支持和使用验证,但你必须使用那个运行器。 MockitoRule:注释支持和任何 JUnit 运行器的使用验证。

另见:How JUnit @Rule works?

【讨论】:

在 Kotlin 中,规则如下所示:@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()【参考方案7】:

有一种巧妙的方法。

如果是单元测试,你可以这样做:

@RunWith(MockitoJUnitRunner.class)
public class MyUnitTest 

    @Mock
    private MyFirstMock myFirstMock;

    @Mock
    private MySecondMock mySecondMock;

    @Spy
    private MySpiedClass mySpiedClass = new MySpiedClass();

    // It's gonna inject the 2 mocks and the spied object per reflection to this object
    // The java doc of @InjectMocks explains it really well how and when it does the injection
    @InjectMocks
    private MyClassToTest myClassToTest;

    @Test
    public void testSomething() 
    

编辑:如果它是一个集成测试,你可以这样做(不打算与 Spring 一起使用。只是展示你可以用不同的 Runners 初始化模拟):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("aplicationContext.xml")
public class MyIntegrationTest 

    @Mock
    private MyFirstMock myFirstMock;

    @Mock
    private MySecondMock mySecondMock;

    @Spy
    private MySpiedClass mySpiedClass = new MySpiedClass();

    // It's gonna inject the 2 mocks and the spied object per reflection to this object
    // The java doc of @InjectMocks explains it really well how and when it does the injection
    @InjectMocks
    private MyClassToTest myClassToTest;

    @Before
    public void setUp() throws Exception 
          MockitoAnnotations.initMocks(this);
    

    @Test
    public void testSomething() 
    

【讨论】:

如果 MOCK 也参与集成测试,是否有意义? 实际上不会,你的权利。我只是想展示 Mockito 的可能性。例如,如果您使用 RESTFuse,您必须使用他们的跑步者,以便您可以使用 MockitoAnnotations.initMocks(this); 初始化模拟;【参考方案8】:

MockitoAnnotations 和跑步者已经在上面进行了很好的讨论,所以我要为不受欢迎的人投掷我的 tuppence:

XXX mockedXxx = mock(XXX.class);

我使用它是因为我发现它更具描述性,并且我更喜欢(不是完全禁止)单元测试不使用成员变量,因为我希望我的测试(尽可能地)自包含。

【讨论】:

除了使测试用例自包含之外,还有什么比使用 mock(XX.class) 的优势吗? 为了阅读测试而必须理解的魔法更少。你声明变量,并给它一个值——没有注释、反射等。

以上是关于初始化模拟对象 - MockIto的主要内容,如果未能解决你的问题,请参考以下文章

Mockito与PowerMock的使用基础教程

怎么mockito方法的内部对象

使用 Mockito 模拟本地范围对象的方法

Android单元测试系列-Mock之Mockito

使用 Spring Boot 和 Mockito 模拟对象方法调用

Mockito + Spring + @PostConstruct,mock初始化错误,为啥会调用@PostConstruct?