Mockito,JUnit 和 Spring

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

你可以跳到 回到我的问题

首先,我所学到的。 这是一篇非常好的文章 嘲笑不是存根,它解释了基础知识(Mock 检查的是 行为验证而不是 国家核查)。这里有一个很好的例子 莫基托 这里是 更容易嘲笑。我们解释了 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());
}

没问题。

回到我的问题。这里 向 Spring bean 中注入 Mockito 模拟有人试图使用 SpringReflectionTestUtils.setField(),但这里 Spring 集成测试,创建模拟对象推荐使用 改变 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个代码示例来澄清我自己: 假设我们有具有方法 printHello()的 bean HelloWorld 和具有方法 sayHello的 bean HelloFacade,它们转发对 HelloWorld 方法 printHello()的调用。

第一个例子是使用 Spring 的上下文,不使用自定义运行程序,而是对依赖注入(DI)使用 RefectionTestUtils:

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 类,而不是接口。

116190 次浏览

如果你使用的是1.9版本(或更新版本) ,你并不真的需要 MockitoAnnotations.initMocks(this);-你所需要的只是:

@InjectMocks
private MyTestObject testObject;


@Mock
private MyDependentObject mockedObject;

@InjectMocks注释将把所有模拟注入到 MyTestObject对象。

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

这就是我对 Spring bean 进行单元测试的方法。没问题。当人们想要使用 Spring 配置文件来实际执行模拟的注入时,他们会遇到困惑,这与单元测试和集成测试的要点相冲突。

当然测试的单元是一个 Impl。你需要测试一个真正具体的东西,对吗?即使您将它声明为一个接口,您也必须实例化真实的东西来测试它。现在,您可以进入间谍,它是围绕真实对象的存根/模拟包装器,但应该是针对角落案例的。

你的问题似乎是问,你给出的三个例子中哪一个是首选的方法。

示例1 使用反射 TestUtils 对于 单元测试来说不是一种好的方法。对于单元测试,您根本不希望加载 Spring 上下文。只需要模仿和注入其他示例所示的内容即可。

如果您想做一些 集成测试操作,那么您确实需要加载 Spring 上下文,但是如果您需要显式地访问它的 bean,那么我更喜欢使用 @RunWith(SpringJUnit4ClassRunner.class)@Autowired一起执行上下文的加载。

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

示例3 是另一种不使用 @RunWith(...)的有效方法。您还没有在测试 HelloFacadeImpl下显式地实例化您的类,但是您可以在示例2中做同样的事情。

我的建议是在单元测试中使用示例2,因为它可以减少代码混乱。如果您被迫这样做,那么可以回到更详细的配置。

老实说,我不确定我是否真正理解你的问题: P 我会尽可能地从你最初的问题中得到解释:

首先,在大多数情况下,您不应该关心 Spring。在编写单元测试时,很少需要涉及 Spring。在正常情况下,您只需要在您的单元测试中实例化被测试的系统(SUT,被测试的目标) ,并在测试中注入 SUT 的依赖项。依赖项通常是 mock/stub。

您最初建议的方法,例子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 集成测试,创建模拟对象中描述的方式。这种方法是在 Spring 的应用程序上下文中创建 mock,您可以从 app ctx 中获得 mock 对象来执行存根/验证:

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


@Autowired
TestTarget sut;


@Autowired
Foo mockFoo;


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

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

只是一个补充,@Mock,@Inject,MocktoJunitRunner 等等在使用 Mockito 时都是不必要的。它们只是一些实用工具,可以帮助您节省输入 Mockito.mock (Foo.class)和一堆 setter 调用的时间。

这是我的简短总结。

如果您想编写一个单元测试,不要使用 Spring applicationContext,因为您不希望在您正在进行单元测试的类中注入任何实际的依赖项。相反,可以使用模拟,或者在类的顶部使用 @RunWith(MockitoJUnitRunner.class)注释,或者在@Before 方法中使用 MockitoAnnotations.initMocks(this)

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

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

例如,使用内存数据库设置应用程序上下文。 通常您不会在集成测试中使用模拟,但是您可以通过使用上面描述的 MockitoAnnotations.initMocks(this)方法来做到这一点。

在 Spring 4.2中引入一些新的测试工具。RC1允许编写不依赖于 SpringJUnit4ClassRunner的 Spring 集成测试。查看文档的 这个部分。

在您的例子中,您可以编写 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() {
// ....
}
}

如果您将项目迁移到 Spring Boot 1.4,您可以使用新的注释 @MockBean来伪造 MyDependentObject。有了这个特性,您可以从测试中删除 Mockito 的 @Mock@InjectMocks注释。