在 TDD 中,您如何为本身具有副作用的代码编写测试?

In the TDD how do you write tests for code that inherently have side effects?

如果功能的副作用是设计中固有的,我该如何开发这样的功能?

例如,如果我想实现像 http.get("url") 这样的函数,并且我将副作用作为具有依赖注入的服务存根,它看起来像:

var http = {
  "get": function( url, service ) {
    return promise(function( resolve ) {
      service( url ).then(function( Response ) {
        resolve( Response );
      });
    });
  }
}

...但是我需要实现与原始 http.get(url) 相同的服务,因此会产生相同的副作用,因此让我处于开发阶段环形。我是否必须模拟服务器来测试这样的功能,如果是的话,那属于 TDD 开发周期的哪一部分?是集成测试,还是单元测试?

另一个例子是数据库模型。如果我正在开发与数据库一起工作的代码,我将设计一个接口,抽象一个实现该接口的模型,然后使用依赖注入将其传递到我的代码中。只要我的模型实现了接口,我就可以使用任何数据库并轻松地存根它的状态和响应,以实现与数据库交互的其他功能的 TDD。那那个模型呢?它将与数据库交互——似乎这种副作用是设计中固有的,当我去实现那个抽象时,将它抽象掉会让我进入一个开发循环。如何在无法将它们抽象掉的情况下实现模型的方法?

如果您正在为这样的模块编写单元测试,请关注该模块本身,而不是依赖项。例如,它应该如何应对 db/service 被关闭,或抛出 exception/error,returning 空数据,returning 良好数据等。这就是为什么你模拟它们和 return 不同的值或设置不同的行为,如抛出异常。

In the TDD how do you write tests for code that inherently have side effects?

我认为我在任何地方都没有看到特别明确的答案;最接近的可能是 GOOS -- TDD 的 "London" 学派倾向于关注外部。

但从广义上讲,您需要意识到副作用属于 imperative shell。它们通常在基础结构组件中实现。因此,您通常需要更高级别的抽象,您可以将其传递给系统的功能部分。

例如,读取系统时钟是一种副作用,会产生自纪元以来的时间值。你的大部分系统不应该关心时间从哪里来,所以读取时钟的抽象应该是系统的输入

现在,感觉就像 "turtles all the way down" -- 您如何测试与基础架构的交互? Kent Beck describes 一个停止条件

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence....

我倾向于Hoare's observation

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies

一旦你着手实施明显正确的副作用,你就不再担心它了。

当你盯着一个副作用,而实施显然不正确时,你开始寻找方法将困难的部分拉回功能核心,进一步隔离副作用。

副作用的实际测试通常发生在您开始将所有组件连接在一起时。由于副作用,这些测试通常较慢;因为它们共享可变状态,所以您通常需要确保它们 运行 顺序。