ChatGPT解决这个技术问题 Extra ChatGPT

Mockito、JUnit 和 Spring

我今天才开始了解 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 定义。我不确定问题是什么,最终。
Spring 对 Mockito 没有任何问题。或相反亦然。
我相信在大多数情况下,您应该针对实际实现而不是接口进行测试。
我就这个问题提出了新问题stackoverflow.com/questions/10937763/…

C
Cleankod

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


@Noam,看看这个答案。阿德里安,很好的答案,谢谢。 :-)
我不认为 1) 有效。也就是说:当同时使用 @Autowired@InjectMocks 时,我在 TestTarget 中看到了 Spring 注入的 bean,而不是模拟。 (我希望同时使用这两个注解Foo 注入一个模拟,但仍然为所有其他在 TestTarget 中自动连接的依赖项使用默认的 Spring 注入 bean,但在集成测试中没有被嘲笑。似乎没有雪茄。Spring 3.1.2;Mockito 1.9.5)
虽然我没有尝试过,但我相信它应该可以工作。 @Autowired 由 Spring JUnit Runner 处理,它在 setup() 之前处理。 @InjectMockssetup() 中的 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 提供一个公共设置器。将后者用于 notFoo 定义公共设置器的目标只会让我在 TestTarget 中获得 Spring 注入的 bean,其中 Spring 注入的 Foonot< /i> 替换为 Mockito 模拟。
D
Dave Newton

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

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

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

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

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

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


g
geoand

在 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() {
         // ....
    }
}

在此处youtube.com/watch?v=-_aWK8T_YMI,您可以观看有关 Java 8 上的 Spring Framework 的视频。
N
Noam

如果您使用的是 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 开始可用
@Brad 与 Mockito 跑步者一起跑步并不总是可行的,仅此而已。
A
Adriaan Koster

这是我的简短总结。

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

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

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

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


j
jhericks

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

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

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


好吧,我真的很想测试我在接口中定义的“合同”。如果我的 Impl 碰巧有一些未通过接口公开的公共方法,对我来说它就像私有方法,我倾向于忽略它。
但是什么能履行“合同”呢?实施者,对吧?所以这就是你测试的。只测试接口会是什么样子?您不能实例化接口。
我就这个问题提出了新问题stackoverflow.com/questions/10937763/…
好的,从您的另一个问题中可以清楚地看出,我们只是用词汇或其他东西混淆了对方。当然你要测试的实例是Impl,但是你声明它是接口还是Impl呢?我认为这是特定于上下文的,但它肯定没有我认为你最初问的那么荒谬。
l
luboskrnac

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