Mockito,JUnit和Spring


76

我直到今天才开始了解Mockito。我写了一些简单的测试(使用JUnit,请参见下文),但是我不知道如何在Spring的托管bean中使用模拟对象。什么是使用Spring的最佳实践。我应该如何向我的bean注入模拟依赖项?

您可以跳过这一步,直到回到我的问题

首先,我学到了什么。这是一篇很好的文章Mocks Are n't Stubs,它解释了基础知识(Mock的检查行为验证而不是状态验证)。然后在这里有一个很好的例子Mockito 和这里更容易用嘲笑嘲笑。我们必须解释是的Mockito的模拟对象都是模拟存根

这里的Mockito这里匹配器,你可以找到更多的例子。

这个测试

@Test
public void testReal(){
    List<String> mockedList = mock(List.class);
     //stubbing
     //when(mockedList.get(0)).thenReturn("first");

    mockedList.get(anyInt());
    OngoingStubbing<String> stub= when(null);
    stub.thenReturn("first");

    //String res = mockedList.get(0);
                //System.out.println(res);

     //you can also verify using argument matcher
     //verify(mockedList).get(anyInt());

    verify(mockedList);
    mockedList.get(anyInt());
}

效果很好。

回到我的问题。这里有人将Mockito模拟注入Spring Bean中,有人尝试使用Springs ReflectionTestUtils.setField(),但是这里我们建议使用Spring Integration Tests,创建Mock对象更改Spring的上下文。

我不太了解最后两个链接...有人可以向我解释Spring对Mockito有什么问题吗?这个解决方案怎么了?

@InjectMocks
private MyTestObject testObject

@Mock
private MyDependentObject mockedObject

@Before
public void setup() {
        MockitoAnnotations.initMocks(this);
}

https://stackoverflow.com/a/8742745/1137529

编辑:我不是很清楚。我将提供3个代码示例来说明我自己:假设,我们有带方法的bean HelloWorld和带方法的printHello()bean HelloFacade,sayHello它们将调用转发给HelloWorld的method printHello()

第一个示例是使用Spring的上下文且没有自定义运行器,将ReflectionTestUtils用于依赖项注入(DI):

public class Hello1Test  {
private ApplicationContext ctx;

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
    this.ctx = new ClassPathXmlApplicationContext("META-INF/spring/ServicesImplContext.xml");
}



@Test
public void testHelloFacade() {
    HelloFacade obj = (HelloFacade) ctx.getBean(HelloFacadeImpl.class);
    HelloWorld mock = mock(HelloWorld.class);
    doNothing().when(mock).printHello();

    ReflectionTestUtils.setField(obj, "hello", mock);
    obj.sayHello();

    verify(mock, times(1)).printHello();
}

}

正如@Noam指出的那样,有一种方法无需显式调用即可运行它MockitoAnnotations.initMocks(this);。我还将在此示例中使用Spring的上下文。

@RunWith(MockitoJUnitRunner.class)
public class Hello1aTest {


@InjectMocks
private HelloFacade obj =  new HelloFacadeImpl();

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

另一种方法

public class Hello1aTest {

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}


@InjectMocks
private HelloFacadeImpl obj;

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

没什么,在前面的示例中,我们必须手动实例化HelloFacadeImpl并将其分配给HelloFacade,因为HelloFacade是接口。在最后一个示例中,我们只需要声明HelloFacadeImpl即可,Mokito将为我们实例化它。这种方法的缺点是,现在,被测单元是impl类,而不是接口。


1
是否有什么问题解决?您链接到的博客文章未使用@InjectMocks(相对较新,尽管IIRC在该博客文章之前),因此在某些情况下可能需要重新排序Bean定义。我不确定问题到底是什么。
戴夫·牛顿

Spring对Mockito没问题。或相反亦然。
罗伯·基尔蒂

我相信在大多数情况下,您应该针对实际的实现而非接口进行测试。
Adrian Shum 2012年

我已经打开了新的问题,关于这个问题stackoverflow.com/questions/10937763/...
alexsmail

Answers:


55

老实说,我不确定我是否真的理解您的问题:PI将尽力从您最初的问题中得到的澄清:

首先,在大多数情况下,您对Spring不应有任何担心。编写单元测试时几乎不需要弹簧。在正常情况下,您只需要在单元测试中实例化被测系统(SUT,要测试的目标),并在测试中注入SUT的依赖项。依赖关系通常是模拟/存根。

您最初建议的方式以及示例2、3正是按照我在上文中的描述进行操作。

在极少数情况下(例如集成测试或某些特殊的单元测试),您需要创建一个Spring应用程序上下文,并从该应用程序上下文中获取SUT。在这种情况下,我相信您可以:

1)在spring app ctx中创建您的SUT,获取引用,然后向其注入模拟

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Before
    /* Initialized mocks */
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void someTest() {
         // ....
    }
}

要么

2)按照链接“ Spring Integration Tests,创建模拟对象”中所述的方法进行操作。这种方法是在Spring的应用程序上下文中创建模拟,您可以从应用程序ctx获取模拟对象以进行存根/验证:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    TestTarget sut;

    @Autowired
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

两种方法都应该起作用。主要区别在于,前一种情况是在经历了Spring的生命周期等之后(例如,bean初始化)注入了依赖项,而后一种情况是事先注入的。例如,如果您的SUT实现了spring的InitializingBean,并且初始化例程涉及到依赖项,您将看到这两种方法之间的区别。我相信这两种方法没有对与错,只要您知道自己在做什么。

只是一个补充,@ Mock,@ Inject,MocktoJunitRunner等在使用Mockito时都是不必要的。它们只是节省您键入Mockito.mock(Foo.class)和一堆setter调用的实用程序。


@Noam,看看这个附加的。阿德里安,很好的回答,谢谢。:-)
alexsmail

2
我不认为1)有效。那就是:当同时使用@Autowiredand时@InjectMocks,我在中看到注入了Spring的bean,而不是模拟对象TestTarget。(我希望同时使用这两个注释 只会为注入一个模拟Foo,但仍对所有其他自动关联TestTarget但在集成测试中未模拟的依赖项使用默认的Spring注入的bean 。看来没有雪茄。 Spring 3.1.2; Mockito 1.9.5)
Arjan

尽管我还没有尝试过,但是我相信它应该可以工作。 @Autowired由Spring JUnit Runner处理,后者在之前处理setup()@InjectMocksMockitoAnnotaitons.initMocks(this)in处理setup()。我没有任何理由停止它的运作。我可以尝试一下以确认它是否有效。:)
Adrian Shum 2012年

7
我确实将包含在MockitoAnnotations.initMocks(this)@Before。另外,删除@Autowired(因此仅留@InjectMocks在原处)确实可以为我带来仿效TestTarget。(但是删除@Autowired也会使Spring保留所有其他未初始化的bean。)但是,进一步的研究表明,我需要一种TestTarget#setFoo(Foo f)方法。没有它,除非与结合使用@InjectMocks否则效果很好@Autowired。因此:当同时使用@Autowired和时@InjectMocks@Autowired private Foo mockFoo;这还不够。可能需要一个错误报告;会调查。
Arjan 2012年

1
不幸的是,使用或确实很重要。对于前者,Spring不会向预期中注入任何东西,并且只要定义私有属性以让Mockito注入其bean就足够了。对于后者,需要为每个Mockito应该注入的bean提供公共设置器。使用后者的,它的目标不是定义一个公有setter只是让我的春天注入豆,春季注射是被嘲笑的Mockito更换。@InjectMocks TestTarget sut@Autowired @InjectMocks TestTarget sutTestTargetTestTargetTestTargetFooTestTargetFoo
Arjan 2012年

6

您的问题似乎是在询问您给出的三个示例中的哪一个是首选方法。

使用Reflection TestUtils的示例1不是进行单元测试的好方法。您真的根本不想为单元测试加载spring上下文。只需模拟并注入其他示例所示的要求即可。

如果要进行一些集成测试,则确实要加载spring上下文,但是我更喜欢使用@RunWith(SpringJUnit4ClassRunner.class)来执行上下文的加载以及@Autowired是否需要显式访问其bean。

示例2是一种有效的方法,使用@RunWith(MockitoJUnitRunner.class)可以消除指定@Before方法和对的显式调用的需要。MockitoAnnotations.initMocks(this);

示例3是另一种不使用的有效方法@RunWith(...)。您尚未HelloFacadeImpl显式实例化要测试的类,但可以使用示例2进行相同的操作。

我的建议是使用示例2进行单元测试,因为它可以减少代码混乱。如果以及当您被迫这样做时,您可以使用更详细的配置。


4

在Spring 4.2.RC1中引入了一些新的测试工具,可以编写不依赖于Java的Spring集成测试SpringJUnit4ClassRunner。看看这个文档的部分。

在您的情况下,您可以编写Spring集成测试,并仍然使用类似以下的模拟:

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @ClassRule
    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

1
这里youtube.com/watch?v=-_aWK8T_YMI可以看到关于Java 8.关于Spring框架的视频
alexsmail

2

MockitoAnnotations.initMocks(this);如果您正在使用Mockito 1.9(或更高版本),则实际上并不需要-您所需要做的就是:

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

@InjectMocks批注会将所有模拟内容注入该MyTestObject对象。


9
它的使用@RunWith(MockitoJUnitRunner.class)消除了MockitoAnnotations.initMocks(this)显式调用的需要。由于这里描述的,因为1.8.3注释已面世
布拉德

1
@Brad并非总是可以与Mockito运行程序一起运行。
戴夫牛顿

2

这是我的简短摘要。

如果要编写单元测试,请不要使用Spring applicationContext,因为您不希望在单元测试的类中注入任何实际的依赖项。而是使用模拟@RunWith(MockitoJUnitRunner.class),或者MockitoAnnotations.initMocks(this)在类顶部使用批注,或者在@Before方法中使用。

如果要编写集成测试,请使用:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("yourTestApplicationContext.xml")

例如,使用内存数据库设置应用程序上下文。通常,您不会在集成测试中使用模拟,但可以使用上述MockitoAnnotations.initMocks(this)方法来进行模拟。


0

是否必须实例化带@InjectMocks注释的字段的区别在于Mockito的版本,而不在于是否使用MockitoJunitRunner或MockitoAnnotations.initMocks。在1.9中,它还将处理@Mock字段的一些构造函数注入,它将为您执行实例化。在早期版本中,您必须自己实例化它。

这就是我对Spring bean进行单元测试的方式。没有问题。人们想要使用Spring配置文件实际进行模拟注入时会感到困惑,这超出了单元测试和集成测试的重点。

当然被测单元是默认地将Impl。您需要测试一个真实的东西,对吗?即使您将其声明为接口,也必须实例化真实的东西才能对其进行测试。现在,您可以进入间谍程序,它们是围绕真实对象的存根/模拟包装程序,但这应该用于极端情况。


好吧,我真的很想测试我在界面中定义的“合同”。如果我的Impl碰巧有一些未通过接口公开的公共方法,对我来说,这就像私有方法,而我倾向于忽略它。
alexsmail 2012年

但是什么满足了“合同”?Impl,对不对?这就是您要测试的内容。仅测试接口会是什么样?您无法实例化接口。
jhericks 2012年

我已经打开了新的问题,关于这个问题stackoverflow.com/questions/10937763/...
alexsmail

好的,从您的其他问题中可以明显看出,我们只是在相互混淆词汇或其他内容。当然,您要测试的实例是Impl,但是您将其声明为接口还是Impl?我认为这是特定于上下文的,但是肯定比我原先以为您要问的要荒谬。
jhericks 2012年

0

如果将项目迁移到Spring Boot 1.4,则可以使用新@MockBean的faking注释MyDependentObject。使用该功能,您可以从测试中删除Mockito@Mock@InjectMocks注释。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.