JUnit - 如何使用不同参数测试相同方法中的重复指令?

JUnit - How to test duplicate instruction in same method with different parameter?

我有一个更新方法,但我在进行单元测试时遇到了问题。在该方法中,我需要验证要更改的对象是否存在于数据库中,我还需要检查用户插入的新数据是否不会导致记录重复。为此,我在两个不同的时间使用 findById():

在数据库中进行搜索
    public void update(Form form, Long storeCode, Long productCode, Long purchaseQuantity) {

    var id = new PrimaryKeyBuilder(DiscountPK.builder().build()).build(storeCode,productCode, purchaseQuantity);
    var targetToUpdate = repository.findById(id).orElseThrow(NotFoundException::new);

    var dataToInsert = SerializationUtils.clone(id);
    dataToInsert.setPurchaseQuantity(form.getPurchaseQuantity());

    var newPk = repository.findById(dataToInsert);
    throwExceptionIf(newPk.isPresent(), new DuplicatedPkException());

    targetToUpdate.getId().setPurchaseQuantity(form.getPurchaseQuantity());
    targetToUpdate.setDiscountPercentage(form.getDiscountPercentage());

    repository.save(targetToUpdate);

}

问题是:我无法在我的单元测试中区分这两个 findById() 指令。我的第一次验证没有成功通过,而是抛出 NotFoundException。就好像第一个 given() 语句被忽略,只考虑第二个语句

    @Test
    public void update_successfully() {

        var targetToUpdate = ObjectFactory.createMain();
        var form = FormFactory.createUpdateForm();
        var id = ObjectFactory.createFirstAux();
        var dataToInsert = ObjectFactory.createSecondAux();

        given(repository.findById(id)).willReturn(Optional.of(targetToUpdate));
        given(repository.findById(dataToInsert)).willReturn(Optional.empty());

        given(repository.save(any())).willReturn(targetToUpdate);

        service.update(form, STORE_CODE, PRODUCT_CODE, PURCHASE_QUANTITY);
        verify(repository).save(targetToUpdate);
    }

奖励代码:Class 构建传入的对象 findById()

 public class PrimaryKeyBuilder {

    private final DiscountPK id;

    public PrimaryKeyBuilder(DiscountPK id) {
        this.id = id;
    }

    public DiscountPK build(Long storeCode, Long productCode, Long purchaseQuantity) {

        id.setPurchaseBoxQuantity(purchaseBoxQuantity);
        id.setProduct(Product.builder().id(ProductPK
                .builder().productCode(productCode).store(Store.builder().code(storeCode).build()).build()).build());

        return id;
    }

}

如果您正在模拟存储库,那么在使用 myRepo.save(myObj);

时它们实际上不会保存更改

不过你可以使用 Mockito 来模拟它:when(X).thenReturn(Y).

例如,

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MyServiceTest {
    
    @Mock private MyRepo myRepo;
    @InjectMocks private MyService myService;

    ...

    @Test
    void myTest() {
        MyClass myObj = new MyClass();
        Mockito.when(myRepo.findById(myObj.getId())
          .thenReturn(Optional.of(myObj));
        // perform desired test
    }
}

使用 givenwhen 定义模拟行为时,您需要提供与被测试代码将提供的参数相匹配的参数。 Mockito documentation 解释说:“Mockito 以自然 java 风格验证参数值:通过使用 equals() 方法。”

如果 DiscountPK class 没有覆盖 equals 方法,将使用 java.lang.Object 中的默认实现。这比较对象身份,并且只有 returns true 当两个对象是同一个实例时。

如果您可以修改 DiscountPK class,最直接的解决方案是覆盖此 class 中的 equalshashCode 方法, 根据它包含的值定义相等性。假设 DiscountPK class 仅包含两个字段,名为 purchaseBoxQuantityproduct,您可以使用 java.util.Objects class 以这种方式定义它们:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DiscountPK that = (DiscountPK) o;
        return Objects.equals(purchaseBoxQuantity, that.purchaseBoxQuantity) && product.equals(that.product);
    }

    @Override
    public int hashCode() {
        return Objects.hash(purchaseBoxQuantity, product);
    }

这还假定 Product class 定义了 equalshashCode

注意,虽然Mockito只使用了equals方法,并没有使用hashCode,但总是建议在覆盖equals时覆盖hashCode,以确保相等对象也有相同的哈希码。

我还建议使主键 classes 不可变。这使得它们更容易推理,因为实体主键的内容永远不应该改变。它还确保 hashCode 的 return 值不会在对象的生命周期内发生变化,这可能会导致错误。