如果功能的副作用是设计中固有的,我该如何开发这样的功能?
例如,如果我想实现一个像 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。不过那个型号呢?它将与数据库交互——似乎这种副作用是设计中固有的,当我去实现那个抽象时,将它抽象出来会让我进入一个开发循环。我如何实现模型的方法而不能将它们抽象出来?
在 TDD 中,如何为本质上具有副作用的代码编写测试?
我认为我没有在任何地方看到过对此问题特别明确的答案;最接近的可能是GOOS ——TDD 的“伦敦”学派倾向于从外到内关注。
但总的来说,您需要意识到副作用属于命令式外壳。它们通常在基础设施组件内实现。因此,您通常需要更高级别的抽象,可以将其传递给系统的功能部分。
例如,读取系统时钟是一个副作用,会产生自纪元以来的时间值。大多数系统不应该关心时间从哪里来,因此读取时钟的抽象应该是系统的输入。
现在,感觉就像“乌龟一直向下”——如何测试与基础设施的交互? Kent Beck 描述了停止条件
我获得报酬的是有效的代码,而不是测试,所以我的理念是尽可能少地进行测试以达到给定的置信度......
我倾向于相信霍尔的观察
构建软件设计有两种方法:一种方法是使其简单到明显没有缺陷
一旦你开始实施明显正确的副作用,你就不再担心它。
当你盯着副作用,并且实现明显不正确时,你开始寻找方法将困难部分拉回功能核心,进一步隔离副作用。
当您开始将所有组件连接在一起时,通常会实际测试副作用。由于副作用,这些测试通常较慢;因为它们共享可变状态,所以您通常需要确保它们按顺序运行。