守卫条款单独需要进行单元测试吗?

时间:2018-07-22 19:27:30

标签: c# unit-testing

TLDR

此方法是否需要单元测试?如果您的回答是“是”,请通过阅读整个问题确保您理解我的思考过程。

public void UpdateChildSomethings(int parentId, string newVal, int bulkSize) {
    var skip = 0;
    List<Child> children = null;
    while ((children = _.GetChildrenFromDB(parentId, skip, bulkSize)).Count > 0) {
        var alteredChildren = AlterChildren(children, newValue); // Note: AlterChildren is fully tested separately.
        _.BulkUpdateDB(alteredChildren);
        skip += bulkSize;
    }
}

前言

首先,我是一个重型单元测试仪。我经常这样做,而且我做得很好。但是根据我的经验,我已经获得了意见,可能需要有人代替我,或者向我提供文档以支持或反对我。

开放免责声明:如果我有一个经过明显测试的方法(例如下面的AlterAlterChildren),并且其中包含保护子句,那么我可能最终会测试保护子句,如果这些测试的覆盖率不超过100%。但是除此之外...

问题

让我们用这种方法开始我的问题:

public void UpdateSomething(int id, string newVal) {
    var actualSomething = _.GetFromDB(id);
    var alteredSomething = Alter(actualSomething, newVal);
    _.UpdateDB(id, alteredSomething);
}

此方法是否需要单元测试?由于多种原因,我个人会拒绝,至少目前不会。特别是如果Alter()经过了大量测试。从数据库获取和更新数据库的操作对于单元测试没有任何价值,并且无论如何都会被模拟。

假设您遵循我的心态并同意不应该测试该方法,那么该方法呢?

public void UpdateSomething(int id, string newVal) {
    var actualSomething = _.GetFromDB(id);
    if (actualSomething == null) return;
    var alteredSomething = Alter(actualSomething, newVal);
    _.UpdateDB(id, alteredSomething);
}

我添加了一个“警卫条款”。这不是业务逻辑或计算。它是确定代码流和提前返回的代码。如果要对此进行单元测试,那么我实际上将在测试GetFromDB的结果,因此将测试模拟。就我而言,测试模拟不是一个有用的测试。

更复杂

但是,假设您仍然遵循我的思想,并同意基于外部数据的保护条款对单元测试是一种浪费,那么这种方法呢?

public void UpdateChildSomethings(int parentId, string newVal, int bulkSize) {
    var skip = 0;
    List<Child> children = null;
    while ((children = _.GetChildrenFromDB(parentId, skip, bulkSize)).Count > 0) {
        var alteredChildren = AlterChildren(children, newValue);
        _.BulkUpdateDB(alteredChildren);
        skip += bulkSize;
    }
}

为清楚起见,我将其重构为分解while子句

/// Uses parentId to retrieve applicable children in chunks of bulkSize.
/// children are processed separately.
/// Passes processed children to the DB to be updated.
public void UpdateChildSomethings(int parentId, string newVal, int bulkSize) {
    var skip = 0;
    List<Child> children = null;
    while (true) {
        children = _.GetChildrenFromDB(parentId, skip, bulkSize);
        if (children.Count == 0) break;
        var alteredChildren = AlterChildren(children, newValue);
        _.BulkUpdateDB(alteredChildren);
        skip += bulkSize;
    }
}

乍一看,这看起来足以测试,但是您要测试什么?再次假设AlterChildren()已经过大量测试,剩下要做的唯一测试就是GetChildrenFromDB()的结果,该结果被模拟。再次测试模拟。这里要做的唯一一行是skip += bulkSize+=运算符在那里将测试什么?我仍然看不到重点。

那么,这是我最复杂的示例,应该对它进行单元测试吗?

2 个答案:

答案 0 :(得分:3)

这里讨论的代码似乎不包含任何业务逻辑。我认为您的意思是:尽管它不包含业务逻辑并且相当琐碎,但是应该对此进行测试吗?

测试“机械”(与业务逻辑相对)没有错。没有理由只能测试业务逻辑。 UpdateSomething为应用程序的其他部分提供服务。您对正确执行该服务感兴趣。

我不太清楚“保护条款”与任何其他逻辑之间的区别。行为与应用程序的功能有关。

您质疑是否要测试基于外部数据的逻辑。我也不认为这是标准。

这些事情使编写测试的可能性更大:代码易于测试;虫子的代价很高;质量对于这段代码很重要;测试不会引起额外的维护工作;该测试不需要对生产代码进行太多更改。

按照这样的具体标准行事。


更新:

  

关于您是否要测试我在“让我们以这种方法开始我的问题”下给出的最简单的代码示例,为什么/否,您是否可以更新您的答案或评论?

好吧,我不能这么说,因为我不知道这对您的测试有多重要。如果您有其他隐式执行此测试的测试,那么我猜我倾向于不对其进行测试。我个人并不热衷于为琐碎的事情编写测试,但是我想这是个人经验的问题。的确,我认为您在问题中提出的标准与该决定完全没有内在联系。该决定应根据我提出的标准做出。这意味着我缺乏做出决定的知识。

在我的职业生涯中,我一次又一次地发现按规则进行编程是行不通的。编程就像国际象棋一样-无限复杂。没有一套规则可以为您做出充分的决策。而是开发一个启发式和模式的思维工具箱,以指导您进行具体案例研究。最后,您必须基于整体的具体情况而不是基于严格的规则来决定。这就是为什么我说“这些事情使测试更有可能”,而不是“您应该在……时进行测试”。

这就是为什么诸如“测试吸气剂和设置器”或“不做测试吸气剂和设置器”之类的规则都是错误的!有时您会测试它们,有时却没有测试。

答案 1 :(得分:0)

您的代码是计算和交互的组合。此类测试的代码对单元测试的价值常常令人怀疑a)由于要进行模拟,b)由于单元测试对实现细节的依赖性。一种经常有用的方法是以一种将计算和交互分离为不同功能/方法的方式来修改代码。然后,使用单元测试对具有计算功能的函数进行测试,而无需或至少不需要模拟,并且使用集成测试对交互主导的函数进行测试。

在您的示例中,计算和交互之间的可能分离如下所示:

public int BulkUpdateChildren(int parentId, string newVal, int skip, int bulkSize) {
    List<Child> children = _.GetChildrenFromDB(parentId, skip, bulkSize);
    if (children.Count > 0) {
        var alteredChildren = AlterChildren(children, newValue);
        _.BulkUpdateDB(alteredChildren);
    }
    return children.Count;
}

public void UpdateChildSomethings(int parentId, string newVal, int bulkSize) {
    var skip = 0;
    var updated;
    do {
        updated = BulkUpdateChildren(parentId, newVal, skip, bulksize);
        skip += updated;
    } while (updated == bulksize);
}

新方法BulkUpdateChildren包含所有与依赖项的交互-最好使用集成测试来测试。 UpdateChildSomethings中剩下的是计算主导的,测试它只需要在SUT本身中模拟一个方法,即对BulkUpdateChildren的调用。因此,单元测试UpdateChildSomethings更容易,并且可以集中于以下问题:在所有可能的情况下skip的值是否正确更新以及循环是否按预期终止。

BulkUpdateChildren中只剩下很少的逻辑,即检查是否找到任何子级,以及返回值的“计算”。如果方法AlterChildrenBulkUpdateDB能够处理空列表,那么甚至不需要检查零个子代(除非出于性能原因)。当退出该检查时,该代码将几乎只包含交互:

public int BulkUpdateChildren(int parentId, string newVal, int skip, int bulkSize) {
    List<Child> children = _.GetChildrenFromDB(parentId, skip, bulkSize);
    var alteredChildren = AlterChildren(children, newValue);
    _.BulkUpdateDB(alteredChildren);
    return children.Count;
}