概述
Mock和Spies都是测试替身的类型,这对编写单元测试很有帮助。
什么是Mock?
Mock替代真实的依赖关系,可以通过编程,在调用mock对象的方法时返回指定的输出。Mockito为mock的所有方法提供了一个空的默认实现。
什么是Spy?
Spy是在mock出来的对象上建立的包装器(Wrapper)。这意味着首先需要一个依赖对象的新实例,然后在此之上添加wrapper。默认地,spy会调用实例真实的方法,除非这个对象的方法被stub。
简而言之,spy:
- 需要对象的真实实例。
- 为被监控对象的部分或全部方法打桩提供了灵活性。这时候,spy调用部分mock或打桩的对象。
- 可以跟踪被监视对象的调用,并进行验证
通常,spy使用并不频繁,但在测试外部依赖不能被完全mock的老旧应用时,spy就很有用。
下面通过一个DiscountCalculator类来示例mockspy的使用。
类有两个方法:
calculateDiscount – 计算给定商品打折后的价格
getDiscountLimit – 获取一个商品打折的上限
创建Mock
1)使用代码创建
Mockito.mock(Class<T> classToMock)
例如为DiscountCalculator创建mock对象:
DiscountCalculator mockedDiscountCalculator = Mockito.mock(DiscountCalculator.class);
需要注意,接口或者实体类都可以创建mock.
当一个对象被mock后,在打桩之前,调用所有方法默认都不会执行实际的工作,返回值也都是null.
2) 使用@Mock注解创建Mock
除了使用Mockito的静态方法mock()创建之外,还提供了一种简便方法:使用 ‘@Mock’ 注解。
这种方式最大的好处是简单,可读性好,并且可以在对象声明或初始化的时候就创建为mock对象。避免同一个mock对象在多处使用时需要重复初始化的麻烦。
@Mock
private transient DiscountCalculator mockedDiscountCalculator;
使用注解的方法创建时,为了Mock能正确地初始化,需要调用 MockitoAnnotations.initMocks(this). 建议将该语句写在beforeEach方法里。
创建Spy
与mock类型,spy也有两种创建方式。
1. 使用Mockito.spy静态方法。只不过需要作用在实例上。
private transient ItemService itemService = new ItemServiceImpl();
private transient ItemService spiedItemService = Mockito.spy(itemService);
2. 使用@Spy 注解创建
@Spy
private transient ItemService spiedItemService = new ItemServiceImpl();
将注解放在实例上就行。也需要注意,在使用spy过的实例前,需先调用MockitoAnnotations.initMocks(this).
在测试用例中为类、对象注入Mock依赖
当我们想要在一个类中使用被mock过的依赖时,也有两种方法。
假设现在我们有一个类PriceCalculator,它有两个依赖:DiscountCalculator和UserService. 我们可以用以下方式为PriceCalculator注入mock过的依赖对象。
1)创建PriceCalculator的实例,并注入依赖。
@Mock
private transient DiscountCalculator mockedDiscountCalculator;
@Mock
private transient UserService userService;
@Mock
private transient ItemService mockedItemService;
private transient PriceCalculator priceCalculator;
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
priceCalculator = new PriceCalculator(mockedDiscountCalculator, userService, mockedItemService);
}
2)通过@InjectMocks注解创建PriceCalculator的实例并注入mock依赖
@Mock
private transient DiscountCalculator mockedDiscountCalculator;
@Mock
private transient UserService userService;
@Mock
private transient ItemService mockedItemService;
@InjectMocks
private transient PriceCalculator priceCalculator;
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
}
InjectMocks通过三种方式尝试注入依赖
- 通过构造函数注入
- 通过Setter方法注入
- 通过类的值(field)注入
技巧&贴士
1)为同一个方法不同的调用,设置不同的打桩
当一个打桩方法在测试中需要被调用多次时(或者被循环调用时),可能希望在每次调用后返回不同的值。
例如,希望ItemService在三次连续调用时返回不同的结果,可以在测试方法中定义多个返回值 item1、item2、item3,然后在打桩时设定这些返回值。
@Test
public void calculatePrice_withCorrectInput_returnsValidResult()
{
// Arrange
ItemSku item1 = new ItemSku();
ItemSku item2 = new ItemSku();
ItemSku item3 = new ItemSku();
// Setup Mocks
when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1, item2, item3);
// Assert
//TODO - add assert statements
}
2)在Mock中抛出异常
测试外部依赖崩溃时,主逻辑的行为是否正常,这是常见的场景。我们可以打桩时通过thenThrow抛出异常。
@Test
public void calculatePrice_withInCorrectInput_throwsException()
{
// Arrange
ItemSku item1 = new ItemSku();
// Setup Mocks
when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString()));
// Assert
//TODO - add assert statements
}
代码样例 - Spy 和 Mock
如前文所说,Spy和Mock作为测试替身的两种类型,它们各有各的用途。
Spy在测试一些老旧的应用(难以mock依赖)时很有用。对于其他书写风格较好的类/方法,Mock就比较适用。
在示例中,我们写一个PriceCalculator#calculatePrice方法的测试用例。被测类和方法如下。
public class PriceCalculator {
public DiscountCalculator discountCalculator;
public UserService userService;
public ItemService itemService;
public PriceCalculator(DiscountCalculator discountCalculator, UserService userService, ItemService itemService) {
this.discountCalculator = discountCalculator;
this.userService = userService;
this.itemService = itemService;
}
public double calculatePrice(int itemSkuCode, int customerAccountId) {
double price = 0;
// get Item details
ItemSku sku = itemService.getItemDetails(itemSkuCode);
// get User and calculate price
CustomerProfile customerProfile = userService.getUser(customerAccountId);
double basePrice = sku.getPrice();
price = basePrice - (basePrice * (sku.getApplicableDiscount() +
customerProfile.getExtraLoyaltyDiscountPercentage()) / 100);
return price;
}
}
首先,我们写一个正向测试用例。
我们给userService和ItemService打桩:
1. UserService总是返回loyaltyDiscountPercentage 为2 的CustomerProfile.
2. ItemService总是返回basePrice = 100, applicableDiscount = 5的Item实例.
3.对于以上输入,被测方法返回的expectedPrice结果是$93.
测试代码如下:
@Test
public void calculatePrice_withCorrectInput_returnsExpectedPrice() {
// Arrange
ItemSku item1 = new ItemSku();
item1.setApplicableDiscount(5.00);
item1.setPrice(100.00);
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 93.00;
// Setting up stubbed responses using mocks
when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1);
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
// Act
double actualPrice = priceCalculator.calculatePrice(123, 5432);
// Assert
assertEquals(expectedPrice, actualPrice);
}
接着,我们写一个使用Spy的测试用例
我们针对ItemService 创建 Spy, 并将其设为总是返回basePrice = 200, applicableDiscount = 10.00% 的item对象。
@InjectMocks
private PriceCalculator priceCalculator;
@Mock
private DiscountCalculator mockedDiscountCalculator;
@Mock
private UserService mockedUserService;
@Spy
private ItemService mockedItemService = new ItemServiceImpl();
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
}
@Test
public void calculatePrice_withCorrectInputRealMethodCall_returnsExpectedPrice()
{
// Arrange
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 176.00;
// Setting up stubbed responses using mocks
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
// Act
double actualPrice = priceCalculator.calculatePrice(2367,5432);
// Assert
assertEquals(expectedPrice, actualPrice);
最后,我们再写一个抛出异常的例子。当item可用数量为0时,ItemService抛出异常。
InjectMocks
private PriceCalculator priceCalculator;
@Mock
private DiscountCalculator mockedDiscountCalculator;
@Mock
private UserService mockedUserService;
@Mock
private ItemService mockedItemService = new ItemServiceImpl();
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
}
@Test
public void calculatePrice_whenItemNotAvailable_throwsException()
{
// Arrange
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 176.00;
// Setting up stubbed responses using mocks
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString()));
// Act & Assert
assertThrows(ItemServiceException.class, () -> priceCalculator.calculatePrice(123, 234));
}
示例完整源码
接口
DiscountCalculator
public interface DiscountCalculator {
double calculateDiscount(ItemSku itemSku, double markedPrice);
void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile);
}
ItemService
public interface ItemService {
ItemSku getItemDetails(int skuCode) throws ItemServiceException;
}
UserService
public interface UserService {
void addUser(CustomerProfile customerProfile);
void deleteUser(CustomerProfile customerProfile);
CustomerProfile getUser(int customerAccountId);
}
接口实现
DiscountCalculatorImpl
public class DiscountCalculatorImpl implements DiscountCalculator {
@Override
public double calculateDiscount(ItemSku itemSku, double markedPrice) {
return 0;
}
@Override
public void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile) {
}
}
ItemServiceImpl
public class DiscountCalculatorImpl implements DiscountCalculator {
@Override
public double calculateDiscount(ItemSku itemSku, double markedPrice) {
return 0;
}
@Override
public void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile) {
}
}
Models
CustomerProfile
public class CustomerProfile {
private String customerName;
private String loyaltyTier;
private String customerAddress;
private String accountId;
private double extraLoyaltyDiscountPercentage;
public double getExtraLoyaltyDiscountPercentage() {
return extraLoyaltyDiscountPercentage;
}
public void setExtraLoyaltyDiscountPercentage(double extraLoyaltyDiscountPercentage) {
this.extraLoyaltyDiscountPercentage = extraLoyaltyDiscountPercentage;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getLoyaltyTier() {
return loyaltyTier;
}
public void setLoyaltyTier(String loyaltyTier) {
this.loyaltyTier = loyaltyTier;
}
public String getCustomerAddress() {
return customerAddress;
}
public void setCustomerAddress(String customerAddress) {
this.customerAddress = customerAddress;
}
}
ItemSku
public class ItemSku {
private int skuCode;
private double price;
private double maxDiscount;
private double margin;
private int totalQuantity;
private double applicableDiscount;
public double getApplicableDiscount() {
return applicableDiscount;
}
public void setApplicableDiscount(double applicableDiscount) {
this.applicableDiscount = applicableDiscount;
}
public int getTotalQuantity() {
return totalQuantity;
}
public void setTotalQuantity(int totalQuantity) {
this.totalQuantity = totalQuantity;
}
public int getSkuCode() {
return skuCode;
}
public void setSkuCode(int skuCode) {
this.skuCode = skuCode;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public double getMaxDiscount() {
return maxDiscount;
}
public void setMaxDiscount(double maxDiscount) {
this.maxDiscount = maxDiscount;
}
public double getMargin() {
return margin;
}
public void setMargin(double margin) {
this.margin = margin;
}
}
被测类
PriceCalculator
public class PriceCalculator {
public DiscountCalculator discountCalculator;
public UserService userService;
public ItemService itemService;
public PriceCalculator(DiscountCalculator discountCalculator, UserService userService, ItemService itemService){
this.discountCalculator = discountCalculator;
this.userService = userService;
this.itemService = itemService;
}
public double calculatePrice(int itemSkuCode, int customerAccountId)
{
double price = 0;
// get Item details
ItemSku sku = itemService.getItemDetails(itemSkuCode);
// get User and calculate price
CustomerProfile customerProfile = userService.getUser(customerAccountId);
double basePrice = sku.getPrice();
price = basePrice - (basePrice* (sku.getApplicableDiscount() +
customerProfile.getExtraLoyaltyDiscountPercentage())/100);
return price;
}
}
测试用例
PriceCalculatorUnitTests
public class PriceCalculatorUnitTests {
@InjectMocks
private PriceCalculator priceCalculator;
@Mock
private DiscountCalculator mockedDiscountCalculator;
@Mock
private UserService mockedUserService;
@Mock
private ItemService mockedItemService;
@BeforeEach
public void beforeEach() {
MockitoAnnotations.initMocks(this);
}
@Test
public void calculatePrice_withCorrectInput_returnsExpectedPrice()
{
// Arrange
ItemSku item1 = new ItemSku();
item1.setApplicableDiscount(5.00);
item1.setPrice(100.00);
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 93.00;
// Setting up stubbed responses using mocks
when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1);
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
// Act
double actualPrice = priceCalculator.calculatePrice(123,5432);
// Assert
assertEquals(expectedPrice, actualPrice);
}
@Test
@Disabled
// to enable this change the ItemService MOCK to SPY
public void calculatePrice_withCorrectInputRealMethodCall_returnsExpectedPrice()
{
// Arrange
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 176.00;
// Setting up stubbed responses using mocks
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
// Act
double actualPrice = priceCalculator.calculatePrice(2367,5432);
// Assert
assertEquals(expectedPrice, actualPrice);
}
@Test
public void calculatePrice_whenItemNotAvailable_throwsException()
{
// Arrange
CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setExtraLoyaltyDiscountPercentage(2.00);
double expectedPrice = 176.00;
// Setting up stubbed responses using mocks
when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile);
when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString()));
// Act & Assert
assertThrows(ItemServiceException.class, () -> priceCalculator.calculatePrice(123, 234));
}
}
最后
以上就是妩媚树叶为你收集整理的使用Mockito创建Mcok和Spy的全部内容,希望文章能够帮你解决使用Mockito创建Mcok和Spy所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复