使用Mockito创建Mcok和Spy

Mock和Spies都是测试替身的类型,这对编写单元测试很有帮助。

什么是Mock?

Mock替代真实的依赖关系,可以通过编程,在调用mock对象的方法时返回指定的输出。Mockito为mock的所有方法提供了一个空的默认实现。

什么是Spy?

Spy是在mock出来的对象上建立的包装器(Wrapper)。这意味着首先需要一个依赖对象的新实例,然后在此之上添加wrapper。默认地,spy会调用实例真实的方法,除非这个对象的方法被stub。

简而言之,spy:

  • 需要对象的真实实例。
  • 为被监控对象的部分或全部方法打桩提供了灵活性。这时候,spy调用部分mock或打桩的对象。
  • 可以跟踪被监视对象的调用,并进行验证

通常,spy使用并不频繁,但在测试外部依赖不能被完全mock的老旧应用时,spy就很有用。

下面通过一个DiscountCalculator类来示例mock\spy的使用。

类有两个方法:

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));
}
 
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值