【问题标题】:Unit Testing Order of Operations单元测试操作顺序
【发布时间】:2026-01-27 07:45:02
【问题描述】:

假设我们想做牛排。

Public class Restaurant{
    public void MakeSteak(Steak rawSteak) {
      this.KitchenServices.AddSeasoning(rawSteak);
      this.KitchenServices.Grill(rawSteak);
    }
}

在单元测试中,我可以确保给定一块生牛排,我们同时对其进行调味和烧烤:

public void MakeSteakTest() {
  var rawSteak = new Steak();
  this.restaurant.MakeSteak(rawSteak);
  this.KitchenServices.Verify(x => x.AddSeasoning(rawSteak) , Times.Once);
  this.KitchenServices.Verify(x => x.Grill(rawSteak) , Times.Once);
}

我的问题是,我们是否应该进行测试以确保牛排在烤完后没有调味?如果是,如何?

【问题讨论】:

标签: c# unit-testing nunit


【解决方案1】:

一般你应该在单元测试中测试结果,而不是实现。如果更改了 MakeSteak 以使操作的顺序不同,您的系统会发生什么情况?那是可以观察到的吗?由于您唯一的输入是Steak 对象,如果在AddSeasoning 之前调用Grill,是否存在某种验证失败? 是你应该测试的。

【讨论】:

  • 我同意我们通常应该避免测试实现细节,但是如果有一些业务逻辑强制我们总是应该在烤牛排之前调味,我怎么能确保这个前提不是在未来的变化中被打破?
  • 再一次,如果您以不同的顺序调用方法,会发生什么?系统状态是否发生了变化?是否抛出异常?如果没有任何变化,那么为什么调用它们的顺序很重要?
【解决方案2】:

一种方法是将每个操作表示为对牛排进行操作的命令对象,例如

interface ISteakOp {
    void DoTo(Steak s);
}

class AddSeasoningOp : ISteakOp { ... }
class GrillOp : ISteakOp { ... }

然后返回一系列操作:

public List<ISteakOp> MakeSteakOps() {
    return new List<ISteakOp> {
        new AddSeasoningOp(),
        new GrillOp()
    };
}

那么您的测试可以断言任何烧烤操作都发生在任何调味之前:

var ops = MakeSteakOps();
var grillIndex = ops.IndexOf(o => o is GrillOp);
if (grillIndex != -1) {
    var seasonIndex = ops.IndexOf(o = o is AddSeasoningOp);
    Assert.That(seasonIndex == -1 || seasonIndex < grillIndex);
}

【讨论】:

    【解决方案3】:

    我的问题是,我们是否应该进行测试以确保牛排在烤完后没有调味?

    首先:它是否对最终用户/利益相关者可见的行为产生影响,他们是否关心?

    如果是,怎么做?

    然后:

    • 如何能够检测到对行为的影响?在您的测试中使用相同的方法。
    • 用户期望的行为是什么?在您的测试中使用这种期望。

    这可能听起来“太简单了”/“不屑一顾”,但我是认真的。这就是正确行为测试的定义。

    【讨论】: