You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 06:05:21