在 Java 中正确使用 mockito ArgumentCaptor?

Proper usage of mockito ArgumentCaptor in Java?

我看过几个使用示例,例如Using Mockito ArgumentCaptor,但我对以下场景的正确用法感到困惑:

这是我要测试的方法:

@Override
public ProductDTO create(Product product, UUID uuid) {
        
    // code omitted for brevity 
        
    final Product saved = productRepository.save(product);
    final Currency currency = currencyService.getCurrencyByUuid(uuid);
        
    return new ProductDTO(saved, currency);
}

我的测试方法:

@RunWith(MockitoJUnitRunner.class)
public class ProductServiceImplTest {

    @Mock
    private ProductRepository productRepository;

    @Mock
    private CurrencyService currencyService;

    @InjectMocks
    private ProductServiceImpl productService;

    @Captor
    private ArgumentCaptor<Product> productCaptor;

    @Test
    public void test_create() {
        UUID uuid = UUID.fromString("00000000-0000-0000-0000-000000000001");
        UUID productUuid = UUID.fromString("00000000-0000-0000-0000-000000000222");
        Currency currency = Currency.getInstance("EUR");        
        Product product = new Product();
        productCostBySite.setProductUuid(productUuid);

        // 1. prepare the values
        when(productRepository.save(productCaptor.capture())).thenReturn(product);
        when(currencyService.getCurrencyByUuid(uuid)).thenReturn(currency);
                
        // 2. call the service method that is tested
        ProductDTO result = productService.create(product);

        // 3. Check if the captured value is the expected one
        Product value = productCaptor.getValue();           
        assertEquals(productUuid, value.getProductUuid());
        assertEquals(currency.getCurrencyCode(), 
            result.getCurrency().getCurrencyCode());
    }
}

题目如下(数字也是测试方法的评论编号):

1. 我使用 ArgumentCaptorcurrencyService.getCurrencyByUuid(uuid) 通过返回值测试了 productRepository.save 方法,因为它不是存储库调用。是真的吗?

2.我调用了服务方法,但是在某些例子中使用了verify方法。我不确定这个例子是否需要使用 verify 方法?有什么想法吗?

3. 我将返回值用于服务,将捕获值用于存储库。这是正确的做法吗?

对我来说,您根本不需要 ArgumentCaptor,您已经模拟了您的存储库和方法 (create) returns 和 ProductDTO。因此,测试此方法有点微不足道。

Product emptyProduct = new Product();
Product productWithValues = new Product();
productWithValues.set... // set the values you want to assert    when(productRepository.save(Mockito.eq(emptyProduct))).thenReturn(productWithValues);

Currency currency = new Currency();
currency.set... // more fields you would like to assert
UUID uuid = UUID.randomUUID();
when(currencyService.getCurrencyByUuid(uuid)).thenReturn(currency);    

ProductDTO result = create(emptyProduct, id);    

Mockito.assertEquals(result.getCurrency().getCurrencyCode(), currency.getCurrencyCode());
Mockito.assertEquals(result.getProduct().getProductUuid(), product.getProductUuid());

声明一下,我没有编译上面的代码,但你应该明白了。

根据documentation推荐的使用ArgumentCaptor的方法不是为了存根,而是为了验证:

verify(productRepository).save(productCaptor.capture())

Argument captor 对于所提供的 create(...) 实现看起来不是特别有用,因为如果您保存的对象与您传递给测试方法的对象完全相同,然后将实际参数传递给 when(...) 足以模拟:

when(productRepository.save(product)).(...)

根据我的经验,当参数未显式传递给被测方法时,更多地使用参数捕获器(在您的示例中,假设 productRepository#save 没有保存传递给 create(...) 的产品,而是在 create(...) 本身内部创建的一些新实例)或者当被测方法更改某些未包含在对象的 equals(...) 方法中的字段时。

应在模拟对象上使用 Verify 来断言实际方法调用使用所需参数发生了特定次数。

具体回答您的问题:

  1. 如果传递给 productRepository#save 的产品与传递给 create(...) 的产品相同,则不需要参数捕获器,因为您只需将 when(productRepository.save(productCaptor.capture())) 替换为 when(productRepository.save(product))
  2. 测试下的方法应该在没有 verify(...) 的情况下被调用,verify 旨在与模拟一起使用以断言特定的模拟方法被调用
  3. 总体方法是正确的

Eugene 是对的,你不需要 ArgumentCaptor。回答您何时需要它的问题:如果您想验证您在方法之前或之后无法访问的转换对象的值。

例如,在

boolean createAndSave(String productName) {
  Product p = new Product(productName);
  return repository.save(p);
}

您无权访问已创建的 Product 但想验证它是否已正确创建。为此,您可以捕获它:

ArgumentCaptor<Product> savedCaptor = ArgumentCaptor.forClass(Product.class);
service.createAndSave("name");
verify(repository).save(savedCaptor.capture()); // get the product being saved
Product saved = savedCaptor.getValue();
assertEqual("name", saved.getName());

现在,您要确保使用存储库保存了您为服务指定的名称的产品。

在您的情况下,您可以将 when(repository.save(any()).thenReturn(product) 与您提前创建的产品一起使用。