如何使用 Verify() 获得特定的预期结果?

How to use Verify() for a specific expected result?

上下文

我正在使用 AutoFixtue 的 GuardClauseAssertion 来验证被测方法是否在任何方法调用参数为 null 时抛出预期的 ArgumentNullException:

var fixture = new Fixture();
var assertion = new GuardClauseAssertion(fixture);
var myMethodInfo = ...
assertion.Verify(myMethodInfo);

被测方法如下:

IEnumerable<T> MyMethod<T>(IList<T> p1, IList<T> p2, Func<T> p3, string p4)
{
     if (p1 == null || p2 == null...)
     {
          yield break;
     }

     // Return non-empty IEnumerable here...
}

问题

在我的特定情况下,预期的行为不是抛出 ArgumentNullException,而是 return 和空 IEnumerable<T>,所以我想验证这个结果。

如何自定义现有的验证默认行为来实现此目标?

GuardClauseAssertion 检查方法参数是否为 null。正如您提到的,您的 特定 案例不符合该行为,因此没有必要使用 GuardClauseAssertion。只需创建一个涵盖您的 special 行为的 special 测试。

同时,如果您使用 assembly-class-wide guard clause verification,您可能需要从验证列表中排除您的特定方法。 AFAIR,AutoFixture 没有为此提供任何 API;你应该只使用 LINQ。

像上面的方法 MyMethod 不是 GuardClauseAssertion 最初设计涵盖的内容。

最初,AutoFixture 是作为一个帮助程序库开发的,用于进行测试驱动开发 (TDD),这一切都是为了获得关于对象设计和实现细节的快速反馈。因为它是一个固执己见的库,AutoFixture 倾向于 增强 TDD 反馈 。当使用 AutoFixture 测试 SUT 变得很尴尬时,可能表明存在更深层次的设计问题。 我倾向于认为这里就是这种情况。

空值是错误的

sir Tony Hoare一样,我认为空引用是错误的。它们是 C# 的一部分,因此我们必须处理它们的潜在存在,但我认为这不应改变 null 不是有效值的基本原则。使用空参数调用方法是错误的。

当客户端开发人员查看

这样的方法签名时
public IEnumerable<Foo> Sut(Bar bar, Baz baz, Qux qux)

在那个级别,没有足够的信息使他或她能够推断出 null 是否被允许用于特定参数。在这个特定的上下文中,您可能已经 'know' 他们都被允许 null,但是客户端开发人员(同事,或者将来您自己)可能不知道所有方法的这一点。

考虑另一种方法:

public Foo CounterExample(Bar bar, Corge corge, Garply garply)

可以用 null bar 呼叫 CounterExample 吗? null corge 呢?如果 bargarplynull 没问题,但如果 corgenull,它会抛出一个 ArgumentNullException .

抽象设计

当整个代码 base/API 被设计成那样(即不一致)时,客户端开发人员获得诸如 哪些参数可以为空的问题的答案的唯一方法。 是阅读实现代码。

这会减慢开发速度,因为每个人都只能在实现细节级别上工作,而不是能够依赖抽象。它还使代码库变得脆弱,因为客户端开发人员最终编写的代码取决于实现细节。更改实现,客户端代码中断。

解决该问题的一种方法是完全删除该问题。不要让客户端开发人员想知道 哪些参数可以为空?,您可以通过全面概括声明在您的代码库中,null 永远不是可接受或有效的值.

因此,每当一个方法接收到一个 null 参数时,它应该抛出一个 ArgumentNullException,这就是 GuardClauseAssertion 旨在验证的内容。

Postel 定律

那么Postel's law呢?它不是说我们接受的东西要自由吗?如果我们可以通过返回一个空的 IEnumerable<T> 来正确处理 null p1,我们不应该这样做吗?

是的,如果那确实是合适的设计,那么这就是 Postel 定律所说的,但我认为它应该在设计中浮出水面。如果一个参数是可选的,这应该用方法重载来建模,而不是允许 null 个参数:

IEnumerable<T> MyMethod<T>()
IEnumerable<T> MyMethod<T>(IList<T> p1)
IEnumerable<T> MyMethod<T>(IList<T> p2)
IEnumerable<T> MyMethod<T>(IList<T> p3)
IEnumerable<T> MyMethod<T>(IList<T> p4)
IEnumerable<T> MyMethod<T>(IList<T> p1, IList<T> p2)
// etc...
IEnumerable<T> MyMethod<T>(IList<T> p1, IList<T> p2, Func<T> p3)
IEnumerable<T> MyMethod<T>(IList<T> p1, IList<T> p2, Func<T> p3, string p4)

这从方法签名中清楚地表明所有参数都是可选的,并且使客户端开发人员不必通读相关方法的实现细节。

这可能会给实施工作增加一些工作量,但可以节省以后的工作量。由于大多数代码的阅读多于编写,因此这针对关键路径进行了优化(即 阅读)。

在这种特殊情况下,由于所有四个参数都是可选的,因此有相当多的组合可用。在这种情况下,我会考虑将设计重构为 Fluent Builder,但在我这样做之前,我会认真地重新考虑我是否真的需要一个方法那么多参数。