cks32和stm32
本文是我们名为“ 用Mockito测试 ”的学院课程的一部分。
在本课程中,您将深入了解Mockito的魔力。 您将了解有关“模拟”,“间谍”和“部分模拟”的信息,以及它们相应的存根行为。 您还将看到使用测试双打和对象匹配器进行验证的过程。 最后,讨论了使用Mockito的测试驱动开发(TDD),以了解该库如何适合TDD的概念。 在这里查看 !
目录
- 1.简介
- 2.模拟,存根,间谍–名称是什么?
- 3.存根方法
- 4.存根返回值
-
- 4.1。 使用答案
- 4.2。 有关行为驱动开发测试约定的说明
- 4.3。 在Eclipse中使用Mockito静态方法的提示
- 4.4。 使用多个模拟
- 4.5。 测试自己! 测试更新!
- 5.参数匹配器
- 6.间谍和部分存根
- 7.结论
- 8.下载源代码
1.简介
在本教程中,我们将深入研究使用Mockito存根类和接口。
2.模拟,存根,间谍–名称是什么?
嘲笑中的许多术语可以互换使用,也可以作为动词和名词使用。 我们现在将对这些术语进行定义,以避免将来造成混淆。
- 模拟(名词) –一个对象,充当另一个对象的双精度对象。
- 模拟(动词) –创建模拟对象或对方法进行存根。
- 间谍(名词) –装饰现有对象并允许对该对象的方法进行存根和对该对象的调用进行验证的对象。
- 间谍(动词) –创建和使用间谍对象。
- 存根(名词) –可以在调用方法时提供“罐头答案”的对象。
- 存根(动词) –创建固定答案。
- Partial Mock,Partial Stub(动词) –间谍的另一种术语,其中某些方法已被禁用。
从技术上讲,Mockito是一个测试间谍框架,而不是模拟框架,因为它使我们能够创建间谍和验证行为,以及创建具有存根行为的模拟对象。
正如在上一教程中所看到的,我们可以使用when().thenReturn()
方法对给定接口或类的行为进行存根。 现在,我们将研究为Mocks和Spies提供存根的所有方法。
3.存根方法
给定以下界面:
public interface Printer {
void printTestPage();
}
以下是使用它的基于字符串缓冲区的简单化“文字处理器”类:
public class StringProcessor {
private Printer printer;
private String currentBuffer;
public StringProcessor(Printer printer) {
this.printer = printer;
}
public Optional statusAndTest() {
printer.printTestPage();
return Optional.ofNullable(currentBuffer);
}
}
我们要编写一个测试方法,该方法将测试构造后是否缺少当前缓冲区并处理测试页的打印。
这是我们的测试课程:
public class StringProcessorTest {
private Printer printer;
@Test
public void internal_buffer_should_be_absent_after_construction() {
// Given
StringProcessor processor = new StringProcessor(printer);
// When
Optional actualBuffer = processor.statusAndTest();
// Then
assertFalse(actualBuffer.isPresent());
}
}
我们知道statusAndTest()
将涉及对Printer
的printTestPage()
方法的调用,并且printer
引用未初始化,因此如果执行此测试,我们将以NullPointerException
结尾。 为了避免这种情况,我们只需要注释测试类以告诉JUnit使用Mockito运行它,并注释Printer作为一个模拟,以告诉mockito为此创建一个模拟。
@RunWith(MockitoJUnitRunner.class)
public class StringProcessorTest {
@Mock
private Printer printer;
@Test
public void internal_buffer_should_be_absent_after_construction() {
// Given
StringProcessor processor = new StringProcessor(printer);
// When
Optional actualBuffer = processor.statusAndTest();
// Then
assertFalse(actualBuffer.isPresent());
}
}
现在我们可以执行测试,Mockito将为我们创建Printer的实现,并将其实例分配给printer变量。 我们将不再获得NullPointerException。
但是,如果Printer
是一类实际完成某些工作的类,例如打印物理测试页,该怎么办? 如果我们选择了@Spy
而不是创建@Mock
怎么办? 记住,除非被侦听,否则间谍会在类上调用间谍的真实方法。 我们希望避免在调用该方法时做任何实际的事情。 让我们做一个简单的Printer实现:
public class SysoutPrinter implements Printer {
@Override
public void printTestPage() {
System.out.println("This is a test page");
}
}
并将其作为间谍添加到我们的测试类中,并添加一个新方法来测试使用它:
@Spy
private SysoutPrinter sysoutPrinter;
@Test
public void internal_buffer_should_be_absent_after_construction_sysout() {
// Given
StringProcessor processor = new StringProcessor(sysoutPrinter);
// When
Optional actualBuffer = processor.statusAndTest();
// Then
assertFalse(actualBuffer.isPresent());
}
如果现在执行此测试,您将在控制台上看到以下输出:
This is a test page
这确认我们的测试用例实际上是在执行SysoutPrinter
类的真实方法,这是因为它是Spy而不是Mock。 如果该类实际执行了测试页的实际物理打印,那将是非常不希望的!
当我们执行部分模拟或Spy时,可以使用org.mockito.Mockito.doNothing()
调用的方法进行存根,以确保其中没有任何org.mockito.Mockito.doNothing()
。
让我们添加以下导入和测试:
import static org.mockito.Mockito.*;
@Test
public void internal_buffer_should_be_absent_after_construction_sysout_with_donothing() {
// Given
StringProcessor processor = new StringProcessor(sysoutPrinter);
doNothing().when(sysoutPrinter).printTestPage();
// When
Optional actualBuffer = processor.statusAndTest();
// Then
assertFalse(actualBuffer.isPresent());
}
注意方法doNothing.when(sysoutPrinter).printTestPage()
:这告诉Mockito当调用@Spy
sysoutPrinter
的void方法printTestPage
,不应执行真正的方法,而应执行任何操作。 现在,当我们执行此测试时,屏幕上看不到任何输出。
如果未连接物理打印机,如果我们扩展打印机接口以引发新的PrinterNotConnectedException
异常,该怎么办? 我们如何测试这种情况?
首先,让我们创建一个非常简单的新异常类。
public class PrinterNotConnectedException extends Exception {
private static final long serialVersionUID = -6643301294924639178L;
}
并修改我们的界面以将其抛出:
void printTestPage() throws PrinterNotConnectedException;
如果抛出异常,我们还需要修改StringProcessor
以执行某些操作。 为了简单起见,我们只将异常抛出给调用类。
public Optional statusAndTest() throws PrinterNotConnectedException
现在我们要测试异常是否传递给调用类,因此我们必须强制打印机抛出异常。 与doNothing()
类似,我们可以使用doThrow
强制执行异常。
让我们添加以下测试:
@Test(expected = PrinterNotConnectedException.class)
public void printer_not_connected_exception_should_be_thrown_up_the_stack() throws Exception {
// Given
StringProcessor processor = new StringProcessor(printer);
doThrow(new PrinterNotConnectedException()).when(printer).printTestPage();
// When
Optional actualBuffer = processor.statusAndTest();
// Then
assertFalse(actualBuffer.isPresent());
}
在这里,我们看到可以使用doThrow()
抛出所需的任何异常。 在这种情况下,我们将抛出满足我们测试要求的PrinterNotConnectedException
。
现在我们已经学习了如何对void方法进行存根,让我们看一下返回一些数据。
4.存根返回值
让我们开始创建一个数据访问对象,以从数据库中持久化和检索客户对象。 该DAO将使用内部的企业java EntityManager
接口进行实际的数据库交互。
为了使用EntityManager
我们将使用JPA 2.0的Hibernate实现,将以下依赖项添加到pom.xml中:
org.hibernate.javax.persistence
hibernate-jpa-2.0-api
1.0.1.Final
现在,我们将创建一个简单的Customer实体来表示要保留的Customer。
@Entity
public class Customer {
@Id @GeneratedValue
private long id;
private String name;
private String address;
public Customer() {
}
public Customer(long id, String name, String address) {
super();
this.id = id;
this.name = name;
this.address = address;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
现在,我们将创建一个骨架DAO,该骨架使用@PersistenceContext
配置注入的EntityManager
。 我们不必担心使用Java持久性体系结构(JPA)或它如何工作-我们将使用Mockito完全绕过它,但这是Mockito实际应用的一个很好的实例。
public class CustomerDAO {
@PersistenceContext
EntityManager em;
public CustomerDAO(EntityManager em) {
this.em = em;
}
}
我们将在DAO中添加基本的“检索和更新”功能,并使用Mockito对其进行测试。
首先使用Retrieve方法-我们将传递一个ID并从数据库中返回适当的Customer(如果存在)。
public Optional findById(long id) throws Exception {
return Optional.ofNullable(em.find(Customer.class, id));
}
在这里,我们使用Java Optional
来避免对结果进行空检查。
现在,我们可以添加测试以在找到客户但找不到客户的地方测试此方法–我们将使用Mockito方法org.mockito.Mockito.when
对find()
方法进行存根处理以在每种情况下返回适当的Optional。然后thenReturn()
让我们如下创建Test类(为Mockito方法import static org.mockito.Mockito.*;
):
@RunWith(MockitoJUnitRunner.class)
public class CustomerDAOTest {
private CustomerDAO dao;
@Mock
private EntityManager mockEntityManager;
@Before
public void setUp() throws Exception {
dao = new CustomerDAO(mockEntityManager);
}
@Test
public void finding_existing_customer_should_return_customer() throws Exception {
// Given
long expectedId = 10;
String expectedName = "John Doe";
String expectedAddress = "21 Main Street";
Customer expectedCustomer = new Customer(expectedId, expectedName, expectedAddress);
when(mockEntityManager.find(Customer.class, expectedId)).thenReturn(expectedCustomer);
// When
Optional actualCustomer = dao.findById(expectedId);
// Then
assertTrue(actualCustomer.isPresent());
assertEquals(expectedId, actualCustomer.get().getId());
assertEquals(expectedName, actualCustomer.get().getName());
assertEquals(expectedAddress, actualCustomer.get().getAddress());
}
}
我们看到了用于启用模仿,模仿EntityManger
并将其注入到测试中的类的常用样板。 让我们看一下测试方法。
第一行涉及创建具有已知期望值的Customer
,然后我们看到对Mockito的调用告诉我们,当使用我们提供的特定输入参数调用EntityManager.find()
方法时,该客户将返回此客户。 然后,我们执行findById()
方法和一组断言的实际执行,以确保我们获得了期望的值。
让我们剖析Mockito调用:
when(mockEntityManager.find(Customer.class, expectedId)).thenReturn(expectedCustomer);
这演示了Mockito强大而优雅的语法。 读起来几乎像普通的英语。 当find()
的方法mockEntityManager
对象被称为与特定输入Customer.class
和expectedId
,然后返回expectedCustomer
对象。
如果您使用未告知其期望的参数调用Mock,则它将仅返回null,如以下测试所示:
@Test
public void invoking_mock_with_unexpected_argument_returns_null() throws Exception {
// Given
long expectedId = 10L;
long unexpectedId = 20L;
String expectedName = "John Doe";
String expectedAddress = "21 Main Street";
Customer expectedCustomer = new Customer(expectedId, expectedName, expectedAddress);
when(mockEntityManager.find(Customer.class, expectedId)).thenReturn(expectedCustomer);
// When
Optional actualCustomer = dao.findById(unexpectedId);
// Then
assertFalse(actualCustomer.isPresent());
}
您还可以在不同的时间对Mock进行存根,以实现不同的行为,具体取决于输入。 让我们让Mock根据输入的ID返回其他客户:
@Test
public void invoking_mock_with_different_argument_returns_different_customers() throws Exception {
// Given
long expectedId1 = 10L;
String expectedName1 = "John Doe";
String expectedAddress1 = "21 Main Street";
Customer expectedCustomer1 = new Customer(expectedId1, expectedName1, expectedAddress1);
long expectedId2 = 20L;
String expectedName2 = "Jane Deer";
String expectedAddress2 = "46 High Street";
Customer expectedCustomer2 = new Customer(expectedId2, expectedName2, expectedAddress2);
when(mockEntityManager.find(Customer.class, expectedId1)).thenReturn(expectedCustomer1);
when(mockEntityManager.find(Customer.class, expectedId2)).thenReturn(expectedCustomer2);
// When
Optional actualCustomer1 = dao.findById(expectedId1);
Optional actualCustomer2 = dao.findById(expectedId2);
// Then
assertEquals(expectedName1, actualCustomer1.get().getName());
assertEquals(expectedName2, actualCustomer2.get().getName());
}
您甚至可以链接返回,以使模拟在每次调用时执行不同的操作。 请注意,如果您调用模拟程序的次数超过了您的存根行为,那么它将永远永远根据最后一个存根行为。
@Test
public void invoking_mock_with_chained_stubs_returns_different_customers() throws Exception {
// Given
long expectedId1 = 10L;
String expectedName1 = "John Doe";
String expectedAddress1 = "21 Main Street";
Customer expectedCustomer1 = new Customer(expectedId1, expectedName1, expectedAddress1);
long expectedId2 = 20L;
String expectedName2 = "Jane Deer";
String expectedAddress2 = "46 High Street";
Customer expectedCustomer2 = new Customer(expectedId2, expectedName2, expectedAddress2);
when(mockEntityManager.find(Customer.class, expectedId1))
.thenReturn(expectedCustomer1).thenReturn(expectedCustomer2);
// When
Optional actualCustomer1 = dao.findById(expectedId1);
Optional actualCustomer2 = dao.findById(expectedId1);
// Then
assertEquals(expectedName1, actualCustomer1.get().getName());
assertEquals(expectedName2, actualCustomer2.get().getName());
}
请注意,我们输入了相同的ID到两个电话,不同的行为是由第二goverened theReturn()
方法,这只能是因为when()
存根的一部分明确预期和输入expectedId1
,如果我们通过expectedId2
我们由于它不是存根中的期望值,因此会从模拟中获得空响应。
现在让我们测试一下缺少客户的情况。
@Test
public void finding_missing_customer_should_return_null() throws Exception {
// Given
long expectedId = 10L;
when(mockEntityManager.find(Customer.class, expectedId)).thenReturn(null);
// When
Optional actualCustomer = dao.findById(expectedId);
// Then
assertFalse(actualCustomer.isPresent());
}
在这里我们可以看到我们使用相同的语法,但是这次使用它来返回null。
允许的Mockito您使用的可变参数thenReturn
存根连续调用,所以如果我们想我们可以在前面的两个测试擀成一个如下:
@Test
public void finding_customer_should_respond_appropriately() throws Exception {
// Given
long expectedId = 10L;
String expectedName = "John Doe";
String expectedAddress = "21 Main Street";
Customer expectedCustomer1 = new Customer(expectedId, expectedName, expectedAddress);
Customer expectedCustomer2 = null;
when(mockEntityManager.find(Customer.class, expectedId)).thenReturn(expectedCustomer1, expectedCustomer2);
// When
Optional actualCustomer1 = dao.findById(expectedId);
Optional actualCustomer2 = dao.findById(expectedId);
// Then
assertTrue(actualCustomer1.isPresent());
assertFalse(actualCustomer2.isPresent());
}
如果我们的find方法由于某些持久性问题而引发异常怎么办? 让我们测试一下!
@Test(expected=IllegalArgumentException.class)
public void finding_customer_should_throw_exception_up_the_stack() throws Exception {
// Given
long expectedId = 10L;
when(mockEntityManager.find(Customer.class, expectedId)).thenThrow(new IllegalArgumentException());
// When
dao.findById(expectedId);
// Then
fail("Exception should be thrown.");
}
我们使用了thenThrow()
方法引发异常。 在对无效方法进行存根时,将此语法与我们对doThrow()
使用进行doThrow()
。 这是两个相似但不同的方法– thenThrow()
将不适用于void方法。
使用答案
上面我们看到我们创建了一个具有某些期望值的客户。 如果我们想创建一些已知的测试用户并以id为基础返回他们,则可以使用Answer
,可以从when()
调用中返回。 Answer
是Mockito提供的通用类型,用于提供“罐头响应”。 它的answer()
方法采用一个InvocationOnMock
对象,该对象包含有关当前模拟方法调用的某些信息。
让我们创建3个客户和一个Answer,根据输入的ID选择要退回的客户。
首先,将3个客户添加为测试类的私有成员。
private Customer homerSimpson, bruceWayne, tyrionLannister;
然后添加一个专用的setupCustomers
方法以对其进行初始化,然后从@Before
方法进行调用。
@Before
public void setUp() throws Exception {
dao = new CustomerDAO(mockEntityManager);
setupCustomers();
}
private void setupCustomers() {
homerSimpson = new Customer(1, "Homer Simpson", "Springfield");
bruceWayne = new Customer(2, "Bruce Wayne", "Gotham City");
tyrionLannister = new Customer(2, "Tyrion Lannister", "Kings Landing");
}
现在,我们可以基于在运行时传递给传递给模拟EntityManager的find()
方法的ID创建一个Answer
来返回适当的Customer。
private Answer withCustomerById = new Answer() {
@Override
public Customer answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
int id = ((Long)args[1]).intValue(); // Cast to int for switch.
switch (id) {
case 1 : return homerSimpson;
case 2 : return bruceWayne;
case 3 : return tyrionLannister;
default : return null;
}
}
};
我们可以看到我们使用InvocationOnMock
提取了传递到Mock方法调用中的参数。 我们知道第二个参数是ID,因此我们可以读取该参数并确定要返回的适当客户。 稍后,带有withCustomerById
的答案的名称将与我们的模拟语法匹配。
现在,让我们编写一个测试来证明此答案的实际效果。
@Test
public void finding_customer_by_id_returns_appropriate_customer() throws Exception {
// Given
long[] expectedId = {1, 2, 3};
when(mockEntityManager.find(eq(Customer.class), anyLong())).thenAnswer(withCustomerById);
// When
Optional actualCustomer0 = dao.findById(expectedId[0]);
Optional actualCustomer1 = dao.findById(expectedId[1]);
Optional actualCustomer2 = dao.findById(expectedId[2]);
// Then
assertEquals("Homer Simpson", actualCustomer0.get().getName());
assertEquals("Bruce Wayne", actualCustomer1.get().getName());
assertEquals("Tyrion Lannister", actualCustomer2.get().getName());
}
让我们详细了解一下存根线。
when(mockEntityManager.find(eq(Customer.class), anyLong())).thenAnswer(withCustomerById);
在这里,我们看到了一些新事物。 第一件事是,我们不执行when().thenReturn()
而是执行when().thenAnswer()
并提供withCustomerById
Answer作为要给出的答案。 第二件事是我们不对传递给mockEntityManager.find()
的ID使用真实值,而是使用静态的org.mockito.Matchers.anyLong()
。 这是一个Matcher
,用于使Mockito发出Answer,而无需检查是否已传入特定的Long值。Matchers让我们忽略模拟调用的参数,而只专注于返回值。
我们还用eq()
Matcher装饰了Customer.class
–这是由于您不能在Mock方法调用中混合使用实值和Matchers,您要么必须将所有参数都作为Matchers,要么必须将所有参数都作为实值。 eq()
提供了一个Matcher,仅当运行时参数等于存根中的指定参数时才匹配。 让我们继续仅在输入类类型为Customer.class类型时不指定特定ID的情况下才返回Answer。
什么这一切意味着,三个调用mockEntityManager.find()
用不同的ID是所有产生相同的答案报错,并且我们已经编码的答案与不同的ID相应的客户对象响应是我们已经成功地嘲笑一个EntityManager
能力模仿现实行为。
有关行为驱动开发测试约定的说明
您可能已经注意到,我们在单元测试中采用了约定,将测试分为三部分– //给定,//时间和//然后。 该约定称为行为驱动开发,是设计单元测试的一种非常合乎逻辑的方法。
- // 给定的是设置阶段,在该阶段我们初始化数据和存根模拟类。 它与陈述“给定以下初始条件”相同。
- //什么时候是执行阶段,在该阶段我们执行被测方法并捕获所有返回的对象。
- //然后是验证阶段,在此阶段我们放置断言逻辑,该逻辑将检查该方法是否表现出预期的行为。
Mockito在org.mockito.BDDMockito
类中开箱即用地支持BDD。 它用BDD doppelgangers – given()
, willReturn()
, willThrow()
, willAnswer()
替换了常规的存根方法– when()
, thenReturn()
, thenThrow()
, thenAnswer()
等。 这样可以避免在// //给定部分中使用when()
,因为这可能会造成混淆。
因为我们在测试中使用BDD约定,所以我们还将使用BDDMockito提供的方法。
让我们使用BDDMockito语法重写finding_existing_customer_should_return_customer()
。
import static org.mockito.BDDMockito.*;
@Test
public void finding_existing_customer_should_return_customer_bdd() throws Exception {
// Given
long expectedId = 10L;
String expectedName = "John Doe";
String expectedAddress = "21 Main Street";
Customer expectedCustomer = new Customer(expectedId, expectedName, expectedAddress);
given(mockEntityManager.find(Customer.class, expectedId)).willReturn(expectedCustomer);
// When
Optional actualCustomer = dao.findById(expectedId);
// Then
assertTrue(actualCustomer.isPresent());
assertEquals(expectedId, actualCustomer.get().getId());
assertEquals(expectedName, actualCustomer.get().getName());
assertEquals(expectedAddress, actualCustomer.get().getAddress());
}
测试的逻辑没有改变,只是以BDD格式可读。
在Eclipse中使用Mockito静态方法的提示
如果要避免导入org.mockito.Mockito.*
等,为各种Mockito静态方法手动添加静态导入可能会很org.mockito.Mockito.*
为了在Eclipse中为这些方法启用内容辅助,您只需启动org.mockito.Mockito.*
> Preferences并转到左侧导航栏中的Java / Editor / Content Assist / Favorites。 然后,按照图1添加以下内容作为“ New Type…”。
-
org.mockito.Mockito
-
org.mockito.Matchers
-
org.mockito.BDDMockito
这会将Mockito静态方法添加到Eclipse Content Assist中,使您可以在使用它们时自动完成并导入它们。
使用多个模拟
现在,我们将结合在一起使用多个模拟。 让我们向DAO中添加一个方法以返回所有可用客户的列表。
public List findAll() throws Exception {
TypedQuery query = em.createQuery("select * from CUSTOMER", Customer.class);
return query.getResultList();
}
在这里,我们看到EntityManager
的createQuery()
方法返回一个通用类型TypedQuery
。 它接受一个SQL String和一个作为返回类型的类作为参数。 TypedQuery
本身公开了几种方法,包括List getResultList()
,这些方法可用于执行返回多个值的查询,例如上面的select * from CUSTOMER
查询中的select * from CUSTOMER
。
为了对此方法编写测试,我们将要创建一个TypedQuery
的Mock。
@Mock
private TypedQuery mockQuery;
现在,我们可以对这个模拟查询进行存根以返回已知客户的列表。 让我们创建一个答案来做到这一点,并重用我们先前创建的已知客户。 您可能已经注意到Answer是一个功能接口,只有一种方法。 我们正在使用Java 8,因此我们可以创建一个lambda表达式来表示我们的内联Answer,而不是像前面的Answer示例中那样创建一个匿名内部类。
given(mockQuery.getResultList()).willAnswer(i -> Arrays.asList(homerSimpson, bruceWayne, tyrionLannister));
当然我们也可以将上面的存根编码为
given(mockQuery.getResultList()).willReturn(Arrays.asList(homerSimpson, bruceWayne, tyrionLannister));given
这展示了Mockito的灵活性–总是有几种不同的方式来做相同的事情。
现在我们已经对模拟TypedQuery
的行为进行了存根,我们可以对模拟EntityManager
进行存根以在请求时返回它。 与其将SQL引入我们的测试用例中, anyString()
仅使用anyString()
匹配器来触发模拟createQuery()
,当然,我们还将用eq()
匹配器包围该类参数。
完整的测试如下所示:
@Test
public void finding_all_customers_should_return_all_customers() throws Exception {
// Given
given(mockQuery.getResultList()).willAnswer(i -> Arrays.asList(homerSimpson, bruceWayne, tyrionLannister));
given(mockEntityManager.createQuery(anyString(), eq(Customer.class))).willReturn(mockQuery);
// When
List actualCustomers = dao.findAll();
// Then
assertEquals(actualCustomers.size(), 3);
}
测试更新!
让我们添加Update()
DAO方法:
public Customer update(Customer customer) throws Exception {
return em.merge(customer);
}
现在查看是否可以为其创建测试。 本教程随附的示例代码项目中已编写了可能的解决方案。 记住,在Mockito中有很多方法可以做相同的事情,看看是否能想到其中的几种!
5.参数匹配器
Mocktio的自然行为是使用对象的equals()
方法作为参数传入,以查看是否存在特定的存根行为。 但是,如果对我们来说不重要的是那些值,则可以在存根时避免使用真实的对象和变量。 我们通过使用Mockito参数匹配器来实现
我们已经看到了一些运行中的Mockito参数匹配器: anyLong()
, anyString()
和eq
。 当我们不特别在意Mock的输入时,我们会使用这些匹配器,我们只对编码它的返回行为感兴趣,并且我们希望它在所有条件下的行为都相同。
如前所述,但需要特别注意的是,当使用参数匹配器时,所有参数都必须是参数匹配器,您不能将实值与参数匹配器混合和匹配,否则会从Mockito中获取运行时错误。
参数匹配器都扩展了org.mockito.ArgumentMatcher
,Mockito包含一个现成的参数匹配器库,可以通过org.mockito.Matchers
的静态方法进行org.mockito.Matchers
,要使用它们只需导入org.mockito.Matchers.*
;
您可以查看org.mockito.Matchers
的javadoc,以查看Mockito提供的所有Matchers,而以下测试类演示了其中一些用法:
package com.javacodegeeks.hughwphamill.mockito.stubbing;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class MatchersTest {
public interface TestForMock {
public boolean usesPrimitives(int i, float f, double d, byte b, boolean bool);
public boolean usesObjects(String s, Object o, Integer i);
public boolean usesCollections(List list, Map map, Set
还可以通过扩展org.mockito.ArgumentMatcher
来创建自己的org.mockito.ArgumentMatcher
。 让我们创建一个匹配器,如果列表包含特定元素,该匹配器将触发。 我们还将创建一个用于创建Matcher的静态便利方法,该方法使用argThat
将Matcher转换为List,以便在存根调用中使用。 我们将实现matches()
方法来调用List
的contains
方法来进行实际的contains检查。
public class ListContainsMatcher extends ArgumentMatcher> {
private T element;
public ListContainsMatcher(T element) {
this.element = element;
}
@Override
public boolean matches(Object argument) {
@SuppressWarnings("unchecked")
List list = (List) argument;
return list.contains(element);
}
public static List contains(T element) {
return argThat(new ListContainsMatcher<>(element));
}
}
现在进行一次测试,以展示我们的新Matcher!
@RunWith(MockitoJUnitRunner.class)
public class ListContainsMatcherTest {
public interface TestClass {
public boolean usesStrings(List list);
public boolean usesIntegers(List list);
}
private List stringList = Arrays.asList("Hello", "Java", "Code", "Geek");
private List integerList = Arrays.asList(1, 2, 3, 4, 5);
@Mock
TestClass test;
@Test
public void test() throws Exception {
when(test.usesStrings(contains("Java"))).thenReturn(true);
when(test.usesIntegers(contains(5))).thenReturn(true);
assertTrue(test.usesIntegers(integerList));
assertTrue(test.usesStrings(stringList));
Mockito.reset(test);
when(test.usesStrings(contains("Something Else"))).thenReturn(true);
when(test.usesIntegers(contains(42))).thenReturn(true);
assertFalse(test.usesStrings(stringList));
assertFalse(test.usesIntegers(integerList));
Mockito.reset(test);
}
}
作为练习,尝试编写自己的Matcher,如果Map包含特定的键/值对,则Matcher将匹配。
6.间谍和部分存根
如前所述,可以使用@Spy
批注对类进行部分存根。 部分存根允许我们在测试中使用真实的类,而仅存根与我们有关的特定行为。 Mockito准则告诉我们,在处理遗留代码时,通常应谨慎偶尔使用间谍。 最佳实践不是使用Spy部分模拟受测类,而是部分模拟依赖项。 被测类应始终是真实对象。
假设我们正在处理一个在java.awt.BufferedImage
上工作的图像处理类。 此类将BufferedImage
放入其构造函数中,并公开一个方法,该方法使用随机彩色的垂直条纹填充图像,并根据输入的缩略图高度返回图像的缩略图。
public class ImageProcessor {
private BufferedImage image;
public ImageProcessor(BufferedImage image) {
this.image = image;
}
public Image overwriteImageWithStripesAndReturnThumbnail(int thumbHeight) {
debugOutputColorSpace();
Random random = new Random();
Color color = new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
for (int x = 0; x < image.getWidth(); x++) {
if (x % 20 == 0) {
color = new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
for (int y = 0; y < image.getHeight(); y++) {
image.setRGB(x, y, color.getRGB());
}
}
}
Image thumbnail = image.getScaledInstance(-1, thumbHeight, Image.SCALE_FAST);
Image microScale = image.getScaledInstance(-1, 5, Image.SCALE_DEFAULT);
debugOutput(microScale);
return thumbnail;
}
private void debugOutput(Image microScale) {
System.out.println("Runtime type of microScale Image is " + microScale.getClass());
}
private void debugOutputColorSpace() {
for (int i=0; i< image.getColorModel().getColorSpace().getNumComponents(); i++) {
String componentName = image.getColorModel().getColorSpace().getName(i);
System.out.println(String.format("Colorspace Component[%d]: %s", i, componentName));
}
}
}
overwriteImageWithStripesAndReturnThumbnail()
方法中发生了很多事情。 它要做的第一件事是输出一些有关图像颜色空间的调试信息。 然后,它会使用图像的宽度和高度方法生成一些随机颜色,并将其绘制为整个图像中的水平条纹。 然后,它执行缩放操作以返回代表缩略图的图像。 然后,它执行第二次缩放操作以生成一个小的诊断微映像,并输出此微映像的运行时类类型作为调试信息。
我们看到了与BufferedImage的许多交互,其中大多数是完全内部的或随机的。 最终,当我们要验证方法的行为时,对我们来说重要的是对getScaledInstance()
的首次调用–如果我们方法的返回值是从getScaledInstance()返回的对象,则类可以工作。 这是BufferedImage的行为,它对我们来说很重要。 我们面临的问题是对BufferedImages方法还有许多其他调用。 从测试的角度来看,我们并不真正在乎这些方法的返回值,但是如果我们不对它们的行为进行编码,则它们将以某种方式导致NullPointerException
并可能导致其他不良行为。
为了解决这个问题,我们将为BufferedImage创建一个Spy,并且仅对我们感兴趣的getScaledInstance()
方法进行存根处理。
让我们创建一个空的测试类,其中包含被测类和Spy类,以及一个用于返回缩略图的Mock。
@RunWith(MockitoJUnitRunner.class)
public class ImageProcessorTest {
private ImageProcessor processor;
@Spy
private BufferedImage imageSpy = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
@Mock
Image mockThumbnail;
@Before
public void setup() {
processor = new ImageProcessor(imageSpy);
}
}
请注意,BufferedImage没有默认构造函数,因此我们必须使用它的参数化构造函数自行实例化它,如果它具有默认构造函数,我们可以让Mockito为我们实例化它。
现在,让我们首先尝试暂存我们感兴趣的行为。忽略输入高度,宽度和模式并继续对所有三个参数使用Argument Matchers是有意义的。 我们最终得到如下内容:
given(imageSpy.getScaledInstance(anyInt(), anyInt(), anyInt())).willReturn(mockThumbnail);
通常,这将是对Spy进行存根的最佳方法,但是,在这种情况下会出现问题– imageSpy是真正的BufferedImage,并且传递given()
Given given()
的存根调用是在存根操作执行时实际执行的真实方法调用由JVM运行。 getScaledInstance
要求width和height不为零,因此此调用将导致引发IllegalArgumentException
。
一种可能的解决方案是在存根调用中使用实参
@Test
public void scale_should_return_internal_image_scaled() throws Exception {
// Given
given(imageSpy.getScaledInstance(-1, 100, Image.SCALE_FAST)).willReturn(mockThumbnail);
// When
Image actualImage = processor.overwriteImageWithStripesAndReturnThumbnail(100);
// Then
assertEquals(actualImage, mockThumbnail);
}
该测试成功运行,并在控制台上产生以下输出
Colorspace Component[0]: Red
Colorspace Component[1]: Green
Colorspace Component[2]: Blue
Runtime type of microScale Image is class sun.awt.image.ToolkitImage
使用实值的副作用是对getScaledInstance()
的第二次调用getScaledInstance()
用于创建用于调试的微图像)无法匹配,并且此时执行了BufferedImage中的real方法,而不是我们的存根行为–这就是为什么我们看到真正的输出的微图像的运行时类型,而不是Mockito模拟实现,我们将查看是否将嘲笑缩略图传递给了调试输出方法。
但是,如果我们想继续使用参数匹配器怎么办? 可以使用doReturn()
方法(如果您记得,通常用于void方法)对getScaledInstance()
方法进行存根,而无需在存根时实际调用它。
@Test
public void scale_should_return_internal_image_scaled_doReturn() throws Exception {
// Given
doReturn(mockThumbnail).when(imageSpy).getScaledInstance(anyInt(), anyInt(), anyInt());
// When
Image actualImage = processor.overwriteImageWithStripesAndReturnThumbnail(100);
// Then
assertEquals(actualImage, mockThumbnail);
}
这给出以下输出:
Colorspace Component[0]: Red
Colorspace Component[1]: Green
Colorspace Component[2]: Blue
Runtime type of microScale Image is class $java.awt.Image$$EnhancerByMockitoWithCGLIB$$72355119
您可以看到微映像的运行时类型现在是Mockito创建的Mock实现。 之所以如此,是因为两个对getScaledInstance
调用getScaledInstance
与存根参数匹配,因此两个调用都返回了Mock缩略图。
有一种方法可以确保在第二个实例中调用Spy的真实方法,方法是使用doCallRealMethod()
方法。 像往常一样,Mockito让您将存根方法链接在一起,以便为与存根参数匹配的存根方法的连续调用编写不同的行为。
@Test
public void scale_should_return_internal_image_scaled_doReturn_doCallRealMethod() throws Exception {
// Given
doReturn(mockThumbnail).doCallRealMethod().when(imageSpy).getScaledInstance(anyInt(), anyInt(), anyInt());
// When
Image actualImage = processor.overwriteImageWithStripesAndReturnThumbnail(100);
// Then
assertEquals(actualImage, mockThumbnail);
}
给出以下输出
Colorspace Component[0]: Red
Colorspace Component[1]: Green
Colorspace Component[2]: Blue
Runtime type of microScale Image is class sun.awt.image.ToolkitImage
7.结论
我们已经研究了许多针对嘲笑和间谍的举止行为的方式,并且暗示了人们可以举止行为的方式几乎无限。
Mockito的javadoc是有关Stubbing方法(尤其是Mockito开箱即用提供的ArgumentMatchers)的良好信息来源。
我们已经详细介绍了存根行为,在下一个教程中,我们将研究使用Mockito验证框架来验证Mocks的行为。
8.下载源代码
这是关于Mockito Stubbing的课程。 您可以在此处下载源代码: mockito2-stubbing
翻译自: https://www.javacodegeeks.com/2015/11/mocks-spies-partial-mocks-and-stubbing.html
cks32和stm32