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

ogg*_*ger 5 tdd side-effects

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

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

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

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

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

Voi*_*son 6

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

我认为我没有在任何地方看到过对此问题特别明确的答案;最接近的可能是GOOS ——TDD 的“伦敦”学派倾向于从外到内关注。

但总的来说,您需要意识到副作用属于命令式外壳。它们通常在基础设施组件内实现。因此,您通常需要更高级别的抽象,可以将其传递给系统的功能部分。

例如,读取系统时钟是一个副作用,会产生自纪元以来的时间值。大多数系统不应该关心时间从哪里来,因此读取时钟的抽象应该是系统的输入

现在,感觉就像“乌龟一直向下”——如何测试与基础设施的交互? Kent Beck 描述了停止条件

我获得报酬的是有效的代码,而不是测试,所以我的理念是尽可能少地进行测试以达到给定的置信度......

我倾向于相信霍尔的观察

构建软件设计有两种方法:一种方法是使其简单到明显没有缺陷

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

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

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