79

我今天才开始了解 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有人尝试使用 Springs ReflectionTestUtils.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 个代码示例来澄清我自己:假设,我们有带有方法的 bean HelloWorld和带有将调用转发到 HelloWorld 方法的方法的printHello()bean HelloFacade 。sayHelloprintHello()

第一个例子是使用 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 而不是接口。

4

7 回答 7

55

老实说,我不确定我是否真的理解你的问题: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 调用的实用程序。

于 2012-06-07T02:22:23.670 回答
6

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

使用反射 TestUtils 的示例 1不是单元测试的好方法。您真的不想为单元测试加载弹簧上下文。只需模拟并注入其他示例所示所需的内容。

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

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

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

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

于 2012-06-07T00:12:34.987 回答
4

在 Spring 4.2.RC1 中引入了一些新的测试工具,让我们可以编写不依赖于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() {
         // ....
    }
}
于 2015-05-26T15:01:22.040 回答
2

这是我的简短总结。

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

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

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

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

于 2015-01-27T09:50:59.563 回答
2

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

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

注释会将您所有的模拟@InjectMocks注入到MyTestObject对象中。

于 2012-06-06T08:07:57.817 回答
0

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

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

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

于 2012-06-06T22:26:11.853 回答
0

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

于 2016-09-05T13:30:07.740 回答