如何用Mockito测试含DTO的save方法?对象不一致问题求解
解决Mockito测试save方法参数不匹配问题
问题场景
测试ProductService的addProduct方法时,服务内部会新建ProductModel对象并赋值,再调用productRepository.save()。但测试代码中手动构造的ProductModel和服务内的实例不是同一个对象,导致verify时提示参数不一致,测试失败。
相关代码
ProductService代码
@Validated public class ProductService { private final CategoryService categoryService; private final ProductRepository productRepository; // 补充原代码遗漏的成员变量 public ProductService(CategoryService categoryService, ProductRepository productRepository){ this.categoryService = categoryService; this.productRepository = productRepository; } public void addProduct(Long categoryId,@Valid AddProductDto addProductDto) throws DataAccessException{ CategoryModel categoryModel = categoryService.getCategoryById(categoryId); ProductModel productModel = new ProductModel(); productModel.setProducent(addProductDto.getProducent()); productModel.setPrice(addProductDto.getPrice()); productModel.setName(addProductDto.getName()); productModel.setSlider(addProductDto.getSlider()); productModel.setImage(addProductDto.getImage()); productModel.setDescription(addProductDto.getDescription()); productModel.setQuantityAvailable(addProductDto.getQuantityAvailable()); productModel.setCategoryModel(categoryModel); productRepository.save(productModel); } }
AddProductDto代码
@Data @Builder public class AddProductDto implements Serializable { @NotEmpty(message = "Wprowadź poprawną nazwę nazwe") @Length(min = 3, max = 220, message = "Wprowadź poprawną długość nazwy przedmiotu") private final String name; @NotNull(message = "Uzupełnij Slider") private final Boolean slider; @NotNull(message = "Wprowadź poprawną ilość") @Min(message = "Wprowadź poprawną ilość", value = 0) private Integer quantityAvailable; @Length(min = 1, max = 220, message = "Wprowadź poprawną długość nazwy producenta") @NotEmpty(message = "Wprowadź poprawnego producenta") private final String producent; @Length(min = 5, max = 220, message = "Wprowadź poprawny opis przedmiotu") @NotEmpty(message = "Wprowadź poprawny opis") private final String description; @NotEmpty(message = "Wprowadź poprawne zdjęcie") @Length(min = 1, max = 240, message = "Wprowadź poprawne zdjęcie przedmiotu") private final String image; @Min(message = "Wprowadź poprawną cenę", value = 1) @NotNull(message = "Wprowadź cenę") private final Double price; }
原测试代码
@Test void testAddProductSuccess(){ //given AddProductDto addProductDto = AddProductDto.builder() .producent("Logitech") .price(1.0) .name("Logitech G-403") .slider(false) .image("myszka1.jpg") .description("Dzięki przemyślanej budowie waży jedynie 59 g, dzięki czemu Twoja dłoń nie męczy się podczas użytkowania.") .quantityAvailable(100) .build(); ProductModel productModel = ProductModel.builder() .producent("Logitech") .price(1.0) .name("Logitech G-403") .slider(false) .image("myszka1.jpg") .description("Dzięki przemyślanej budowie waży jedynie 59 g, dzięki czemu Twoja dłoń nie męczy się podczas użytkowania.") .quantityAvailable(100) .build(); Set<ConstraintViolation<AddProductDto>> violations = validator.validate(addProductDto); //when productService.addProduct(1L,addProductDto); //verify verify(productRepository,times(1)).save(productModel); assertTrue(violations.isEmpty()); }
测试错误信息
Argument(s) are different! Wanted: productRepository.save( model.ProductModel@55a88417 ); Actual invocations have different arguments: productRepository.save( ProductModel@18acfe88 );
解决方案
方法1:使用Mockito的ArgumentMatchers.refEq()
refEq()会递归比较对象的属性值,而非对象引用。可以指定忽略某些不需要比较的属性(比如服务中额外设置的categoryModel)。
修改测试中的verify部分:
// 忽略categoryModel属性的比较,因为测试构造的productModel没有设置该属性 verify(productRepository, times(1)).save(ArgumentMatchers.refEq(productModel, "categoryModel"));
方法2:给ProductModel实现equals和hashCode
如果ProductModel使用Lombok的@Data或@EqualsAndHashCode注解,会自动生成基于属性的equals和hashCode方法,这样属性相同的对象会被判定为相等。
示例ProductModel代码:
@Data @Builder public class ProductModel { private String producent; private Double price; private String name; private Boolean slider; private String image; private String description; private Integer quantityAvailable; private CategoryModel categoryModel; }
修改后原测试的verify代码无需改动,属性匹配即可通过验证。
方法3:使用ArgumentCaptor捕获参数后验证
通过ArgumentCaptor捕获save方法的实际参数,然后逐一验证属性值,这种方式更灵活,能全面验证所有属性(包括categoryModel)。
修改后的测试代码:
@Test void testAddProductSuccess(){ // given AddProductDto addProductDto = AddProductDto.builder() .producent("Logitech") .price(1.0) .name("Logitech G-403") .slider(false) .image("myszka1.jpg") .description("Dzięki przemyślanej budowie waży jedynie 59 g, dzięki czemu Twoja dłoń nie męczy się podczas użytkowania.") .quantityAvailable(100) .build(); // Mock categoryService的返回值 CategoryModel mockCategory = CategoryModel.builder().id(1L).build(); when(categoryService.getCategoryById(1L)).thenReturn(mockCategory); Set<ConstraintViolation<AddProductDto>> violations = validator.validate(addProductDto); // 定义捕获ProductModel的ArgumentCaptor ArgumentCaptor<ProductModel> productCaptor = ArgumentCaptor.forClass(ProductModel.class); // when productService.addProduct(1L, addProductDto); // verify verify(productRepository, times(1)).save(productCaptor.capture()); ProductModel capturedProduct = productCaptor.getValue(); // 逐一验证属性 assertEquals(addProductDto.getProducent(), capturedProduct.getProducent()); assertEquals(addProductDto.getPrice(), capturedProduct.getPrice()); assertEquals(addProductDto.getName(), capturedProduct.getName()); assertEquals(addProductDto.getSlider(), capturedProduct.getSlider()); assertEquals(addProductDto.getImage(), capturedProduct.getImage()); assertEquals(addProductDto.getDescription(), capturedProduct.getDescription()); assertEquals(addProductDto.getQuantityAvailable(), capturedProduct.getQuantityAvailable()); assertEquals(mockCategory, capturedProduct.getCategoryModel()); assertTrue(violations.isEmpty()); }
内容的提问来源于stack exchange,提问作者asdasdas
相关产品推荐
相关产品推荐

