对于共享相同私有实现的单独方法,是否应该有重复的单元测试

本文关键字:单独 方法 是否 单元测试 实现 于共享 共享 | 更新日期: 2023-09-27 17:58:43

我正在为我们的大型企业应用程序构建业务层,目前我们正在进行不到500次单元测试。场景是,我们有两个public方法,public AddT(T)public UpdateT(T),它们都对private AddOrUpdateT(T)进行内部调用,因为两者之间的许多核心逻辑是相同的,但不是全部;它们是不同的。

因为它们是独立的公共API(无论私有实现如何),所以我为每个API编写了单元测试,尽管它们是相同的。这可能看起来像

[TestMethod]
public void AddT_HasBehaviorA() 
{
}
[TestMethod]
public void UpdateT_HasBehaviorA() 
{ 
}

目前,对于这个特定的业务对象,大约有30个单元测试需要添加,40个单元测试用于更新,其中30个更新测试与添加测试相同。

这是正常的吗?还是我应该将常见行为抽象到一个单独进行单元测试的公共助手类中,而两个API只使用该助手类,而不是一个有实现的私有方法?

对于这些情况,什么是最佳实践?

对于共享相同私有实现的单独方法,是否应该有重复的单元测试

首先,理解为什么要通过单元测试来覆盖这些方法很重要,因为这会影响答案。只有您和您的团队知道这一点,但如果我们假设单元测试的至少部分动机是获得值得信赖的回归测试套件,那么您应该测试被测系统(SUT)的可观察行为

换句话说,单元测试应该是黑盒测试。测试不应该知道SUT的实现细节。因此,您可以从中得出一个天真的结论,即如果您有重复的行为,您也应该有重复的测试代码。

然而,你的系统变得越复杂,越依赖于常见的行为和策略,就越难实现这种测试策略。这是因为您将通过系统进行组合爆炸的可能方式。J.B.Rainsberger比我解释得更好。

更好的选择通常是听测试(GOOS推广的一个概念)。在这种情况下,将常见行为提取到公共方法中似乎很有价值。然而,这本身并不能解决组合爆炸的问题。虽然您现在可以单独测试常见行为,但还需要证明两个原始方法(AddUpdate使用新的公共方法(而不是,例如,一些复制粘贴的代码)。

做到这一点的最佳方法是用新策略组成方法:

public class Host<T>
{
    private readonly IHelper<T> helper;
    public Host(IHelper<T> helper)
    {
        this.helper = helper;
    }
    public void Add(T item)
    {
        // Do something
        this.helper.AddOrUpdate(item);
        // Do something else
    }
    public void Update(T item)
    {
        // Do something
        this.helper.AddOrUpdate(item);
        // Do something else
    }
}

(显然,你需要给类型和方法起更好的名字。)

这使您能够使用行为验证来证明AddUpdate方法正确使用AddOrUpdate方法:

[TestMethod]
public void AddT_HasBehaviorA() 
{
    var mock = new Mock<IHelper<object>>();
    var sut = new Host<object>(mock.Object);
    var item = new object();
    sut.Add(item);
    mock.Verify(h => h.AddOrUpdate(item));
}
[TestMethod]
public void UpdateT_HasBehaviorA() 
{ 
    var mock = new Mock<IHelper<object>>();
    var sut = new Host<object>(mock.Object);
    var item = new object();
    sut.Update(item);
    mock.Verify(h => h.AddOrUpdate(item));
}

尽可能避免重复是一种很好的做法。这有助于代码的可读性和可维护性(可能还有其他*功能;-))。它还使测试变得更容易,因为您可以在一个地方开始测试通用功能,而不必有那么多重复的测试。此外,测试中的重复也很糟糕。

另一个最佳实践是只为具有唯一逻辑的功能编写单元测试。如果在Add和Update方法中,您只调用另一个方法,则不需要在该级别编写单元测试,您应该专注于被调用的方法。

这又回到了不重复代码的最初点,如果你有共享重复代码的私有方法,那么把它分解成其他可以运行测试的东西可能是个好主意。

"因为两者之间的许多核心逻辑是相同的,但不是全部;它们是不同的。"

我认为你应该对这些进行单独的单元测试,因为正如你所说的,它们并不完全相同。此外,如果您稍后更改实现以调用两种不同的方法,该怎么办?然后,您的单元测试将只测试其中一个,因为它们与两个方法的实现细节相耦合。测试会通过,但您可能引入了一个错误。

我认为更好的方法是测试这两个方法,但添加辅助方法/类来完成常见的工作。