我今天才开始了解 Mockito。我写了一些简单的测试(使用 JUnit,见下文),但我不知道如何在 Spring 的托管 bean 中使用模拟对象。使用 Spring 的最佳实践是什么。我应该如何向我的 bean 注入模拟依赖项?
你可以跳过这个直到回到我的问题。
首先,我学到了什么。这是一篇很好的文章 Mocks Aren't Stubs,它解释了基础知识(Mock 的检查行为验证而不是状态验证)。然后这里有一个很好的例子 Mockito 和这里 Easier mocking with mockito。我们解释说 Mockito 的模拟对象既是 mock 又是 stub。
在此处 Mockito 和此处 Matchers,您可以找到更多示例。
本次测试
@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());
}
工作得很好。
回到我的问题。这里Injecting Mockito mocks into a Spring bean有人尝试使用 SpringsReflectionTestUtils.setField()
,但这里Spring Integration Tests, Creating Mock Objects我们建议更改 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 的上下文并且没有自定义运行器,使用 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-class 而不是接口。
@InjectMocks
(相对较新,尽管 IIRC 在该博客文章之前),因此在某些情况下可能需要重新排序 bean 定义。我不确定问题是什么,最终。
老实说,我不确定我是否真的理解你的问题:PI 会尽量澄清,从我从你原来的问题中得到的:
首先,在大多数情况下,您不应该对 Spring 有任何顾虑。您很少需要让 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, Creating Mock Objects 中描述的方式进行操作。这种方法是在 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 调用的实用程序。
您的问题似乎是在询问您给出的三个示例中的哪一个是首选方法。
使用反射 TestUtils 的示例 1 不是单元测试的好方法。您真的不想为单元测试加载弹簧上下文。只需模拟并注入其他示例所示所需的内容。
如果您想进行一些集成测试,您确实想加载 spring 上下文,但是如果您需要访问它,我更喜欢使用 @RunWith(SpringJUnit4ClassRunner.class)
和 @Autowired
来执行上下文的加载' 明确的 bean。
示例 2 是一种有效的方法,使用 @RunWith(MockitoJUnitRunner.class)
将无需指定 @Before 方法和显式调用 MockitoAnnotations.initMocks(this);
示例 3 是另一种不使用 @RunWith(...)
的有效方法。您尚未显式实例化您的测试类 HelloFacadeImpl
,但您可以对示例 2 执行相同操作。
我的建议是使用示例 2 进行单元测试,因为它可以减少代码混乱。如果您被迫这样做,您可以回退到更详细的配置。
在 Spring 4.2.RC1 中引入了一些新的测试工具,可以编写不依赖于 SpringJUnit4ClassRunner
的 Spring 集成测试。查看文档的 this 部分。
在您的情况下,您可以编写 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() {
// ....
}
}
如果您使用的是 mockito 1.9(或更新版本),您实际上并不需要 MockitoAnnotations.initMocks(this);
- 您所需要的只是:
@InjectMocks
private MyTestObject testObject;
@Mock
private MyDependentObject mockedObject;
@InjectMocks
注释会将您的所有模拟注入 MyTestObject
对象。
@RunWith(MockitoJUnitRunner.class)
消除了显式调用 MockitoAnnotations.initMocks(this)
的需要。作为 described here,注释从 1.8.3 开始可用
这是我的简短总结。
如果您想编写单元测试,请不要使用 Spring applicationContext,因为您不希望在您正在单元测试的类中注入任何真正的依赖项。而是使用模拟,或者在类顶部使用 @RunWith(MockitoJUnitRunner.class)
注释,或者在 @Before 方法中使用 MockitoAnnotations.initMocks(this)
。
如果要编写集成测试,请使用:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("yourTestApplicationContext.xml")
例如,使用内存数据库设置应用程序上下文。通常您不会在集成测试中使用模拟,但您可以使用上述 MockitoAnnotations.initMocks(this)
方法来做到这一点。
是否必须实例化 @InjectMocks
注释字段的区别在于 Mockito 的版本,而不在于您使用 MockitoJunitRunner 还是 MockitoAnnotations.initMocks
。在 1.9 中,它还将处理您的 @Mock
字段的一些构造函数注入,它将为您进行实例化。在早期版本中,您必须自己实例化它。
这就是我对 Spring bean 进行单元测试的方式。没有问题。当人们想要使用 Spring 配置文件来实际进行 mock 的注入时,人们会感到困惑,这跨越了单元测试和集成测试的点。
当然,被测单元是一个 Impl。你需要测试一个真正具体的东西,对吧?即使你将它声明为一个接口,你也必须实例化真实的东西来测试它。现在,您可以进入间谍,它们是围绕真实对象的存根/模拟包装器,但这应该是针对极端情况的。
如果您要将项目迁移到 Spring Boot 1.4,则可以使用新注释 @MockBean
来伪造 MyDependentObject
。使用该功能,您可以从测试中删除 Mockito 的 @Mock
和 @InjectMocks
注释。
@Autowired
和@InjectMocks
时,我在TestTarget
中看到了 Spring 注入的 bean,而不是模拟。 (我希望同时使用这两个注解仅为Foo
注入一个模拟,但仍然为所有其他在TestTarget
中自动连接的依赖项使用默认的 Spring 注入 bean,但在集成测试中没有被嘲笑。似乎没有雪茄。Spring 3.1.2;Mockito 1.9.5)@Autowired
由 Spring JUnit Runner 处理,它在setup()
之前处理。@InjectMocks
由setup()
中的MockitoAnnotaitons.initMocks(this)
处理。我看不出有任何理由停止它的工作。我可能会尝试一下以确认它是否有效。 :)@Before
中包含了MockitoAnnotations.initMocks(this)
。此外,删除@Autowired
(因此只保留@InjectMocks
)确实给了我在TestTarget
中的模拟。 (但删除@Autowired
也会使 Spring 未初始化所有其他 bean。)但是,更多调查表明我需要一个TestTarget#setFoo(Foo f)
方法。没有它,@InjectMocks
就可以正常工作,除非 与@Autowired
结合使用。所以:如果同时使用@Autowired
和@InjectMocks
,那么@Autowired private Foo mockFoo;
是不够的。 可能需要错误报告;将调查。@InjectMocks TestTarget sut
或@Autowired @InjectMocks TestTarget sut
确实。对于前者,Spring 不会向TestTarget
注入任何东西(如预期的那样),如果TestTarget
定义私有属性以让 Mockito 注入其 bean,这就足够了。对于后者,TestTarget
需要为每个 Mockito 应该注入的 bean 提供一个公共设置器。将后者用于 not 为Foo
定义公共设置器的目标只会让我在TestTarget
中获得 Spring 注入的 bean,其中 Spring 注入的Foo
是 not< /i> 替换为 Mockito 模拟。