Week4 - 写测试的RD竟然比没写测试的RD开发得更快!?这是不是搞错了什么 [Server的终局之战系列]

嗨大家好,在写「鼠年全马铁人挑战-NodeJs转Golang的爆炸之旅系列」时,其实有时候也会想写其他东西,所以以后会依照每週不同的想法来撰写,毕竟40週如果一直写一个系列,说实在的有点难受,因为这么长的时间会一直接触到不同的东西,有时候碰到有趣的东西不能分享是有些痛苦的XD

所以另开了一个系列「鼠年全马铁人挑战-Server的终局之战系列」


文章也同时发表于medium(`・ω・´)”

关于测试,一直有一张有名的meme图

为什么改一下code,会导致debug那么久呢?最主要是因为副作用-side effect,白话来说就是你改的这些code其实会影响到你预期以外的地方,举个例子,以下是一个读json文件的function,并且可以自订json文件里面的ps栏位:

const jsonFile = JSON.parse(fs.readFileSync('./文件.json'))function readFile(ps) {    jsonFile.ps = ps    return jsonFile}const one = readFile('第一次读')const two = readFile('第二次读')console.log(one) //ps栏位也变成'第二次读'console.log(two) //ps栏位为'第二次读'

因为jsonFile是由「外部」引入的,所以在第二次呼叫readFile function的时候其实不知不觉把第一次呼叫的结果one给改了,这就是副作用的一种,那要怎么解决呢?我们可以把程式码改成如下:

const jsonFile = JSON.parse(fs.readFileSync('./文件.json'))function readFile(ps, jsonFile) {    return {...jsonFile, ps: ps}}const one = readFile('第一次读', jsonFile)const two = readFile('第二次读', jsonFile)console.log(one) //ps栏位为'第一次读'console.log(two) //ps栏位为'第二次读'

readFile的jsonFile改为由外部带入,而readFile里面的jsonFile也重新copy了这个jsonFile,并重新赋值,这使jsonFile与外部的jsonFile不相依,就不会改到同个变数。

而自从我发生了无数次副作用又在那边找很久后...

于是就有了TDD

TDD就是

先写测试,再开发功能,步骤为:

先规划好测试快速开发feature,使feature通过测试依照完成的feature进行重构,重构时可以透过测试审视自己的code是否因重构导致错误,所以可以快速重构并且不怕搞烂自己的code

但在许多的RD眼裏,测试一直是拖慢开发速度的一环,主要的原因就是这些新feature测试起来,可能得事先调整很多配置,举个例子来说,髒沙发辨识AV女优的大致流程如下:

现在有个新功能要将髒沙发的讯息模板由这样:

改成这样:

那在开发上很直观会这样做:

「将女优资讯套用到讯息模板上」这段程式逻辑做调整将照片上传至髒沙发主-server髒沙发主-server传送照片至辨识-server辨识-server再传回髒沙发主-server我看看是不是最后回来的讯息正确正确就收工惹(。´∀`)ノ,如果不正确就重複1~5步(。ヘ°)

因为2~5步其实都是「重複性」的测试工作,所以会有人提出「把这几步写成自动化测试啊!」的说法,但坦白说,这是件非常麻烦的事情。

首先你要将辨识-server部署好,并且将辨识-server的资料库与辨识-server连结,最后要确保辨识-server的运作一切正常,才能开始写「将女优资讯套用到讯息模板上」这段程式。

这导致很多RD会放弃测试,因为不断重複2~5步的开发虽然拢,但开发上还是比写测试的时间来的快,甚至觉得所谓「先写测试在写程式功能的TDD是个谬论」,但为什么测试会那么麻烦呢?主要就是:

测试的所相依的事物太多了

我们回到最一开始,其实我们这个feature的目标是「AV女优面板做调整」,那要测试时我就测「将女优资讯套用到讯息模板上」这段逻辑就好了,辨识-server是可以排除掉的,所以可以把辨识-server的输入输出用「假资料」替换掉,即会变成这样:

而流程就会变成这样:

可以看到,因为跟其他server都没有相依,所以写测试更为容易,这就是我们所谓的单元测试-unit test,大家可以看看测试金字塔,


         photo by lawrey medium

越往金字塔的顶端即:

测试的所整合的範围更多测试所带来的成本越大

越往金字塔的底部即:

测试的所整合的範围越少测试所带来的成本越小

而单元测试即是最底部的,他把许多的外部相依都靠测试替身-test double来排除。

以程式码来说:

// main.jsconst request = require('./request')const fs = require('fs')async function searchFace(imagePath) {    return await request({        url: '辨识-server',        formdata: {image: fs.createReadStream(imagePath)}    })}async function applyTemplate(imagePath, faceProvider) {    const faceResult = await faceProvider(imagePath)    return {        name: faceResult.name,        description: faceResult.detail.description,        similarity: faceResult.detail.similarity    }}// test.jsconst main = require('./main')test('当套用模板时,会回传正确值', () => {    const mock辨识server = jest.fn().mockReturnValue({        name: '桥本有菜',        detail: {            description: '很可爱',            similarity: '83%'        }    });    expect(main.applyRecognitionTemplate('./test.jpg',mock辨识server).toStrictEqual({        name: '桥本有菜',        description: '很可爱',        similarity: '83%'    })})

可以看到其实我们利用faceProvider这个参数,让去call辨识-server的角色有了弹性,我们可以编造假的mock辨识server,它的输入输出是由我们测试所定好,并不会因为辨识-server自己出现了什么问题,而导致我们测试上的误判。

所以这个测试我们可以focus在applyTemplate function的这段code:

return {        name: faceResult.name,        description: faceResult.detail.description,        similarity: faceResult.detail.similarity    }

因为如果出错了,一定只有这边有问题,不可能是辨识server的部分。

这边我们发现到一个很有趣的现象,就是似乎我们的「程式码要配合unit test来撰写」,是的没错,假设我们将searchFace function写在applyTemplate function里面,

// main.jsconst request = require('./request')const fs = require('fs')async function searchFace(imagePath) {    return await request({        url: '辨识-server',        formdata: {image: fs.createReadStream(imagePath)}    })}async function applyTemplate(imagePath) {    const faceResult = await searchFace(imagePath)    return {        name: faceResult.name,        description: faceResult.detail.description,        similarity: faceResult.detail.similarity    }}

这时候我们就会面临无法替换searchFace的现象,因为searchFace被写死了,这导致与applyTemplate强耦合。

所以程式在写的时候,其实也要考虑程式的可测试性-testable,这代表:

先上程式码开发的车再候补测试的票,成本是会越来越大的

因为一开始一直没考虑可测试性所以后来要测unit test就会遇到频频无法隔离的状况,

当然如果是开发很有经验的人,就算没有测试也可以让各个程式码的耦合降低,但事先有配合测试,广义上来说会因测试被动解耦,这是我在写的时候遇到的有趣现象,

而这边使用faceProvider是以物件导向SOLID法则的依赖反转原则来进行设计,依照wiki来说即是

高层次的模组不应该依赖于低层次的模组,两者都应该依赖于抽象介面。抽象介面不应该依赖于具体实现。而具体实现则应该依赖于抽象介面。

听起来非常的複杂,但其实以这个例子广义来讲就是:

不要把searchFace function写死在applyTemplate function里面,不然我要怎么换他啦ヘ(゜Д、゜)ノ

另外也可以在require的时候引入test double,这也是一种做法

结论

我们可以靠unit test来加快我们写code验证迭代的速度,依照「重构|改善既有程式的设计」这本书来说

在重构之前,请先準备好坚实的测试程式,测试程式必须是自检的重构就是用小步骤修改程式,让轻鬆的找到bug的位置

可以理解到有配合unit test开发,因为减少了一定的debug成本,所以有许多机会是比没写测试的人开发来得更快,因为:

外部相依少,所以可以每save一次程式码就跑一次,因为测试程式是自检的因为跑得快,你可以改一行小code就save一下,看看测试有没有过,如果测试错了就代表这一行小code有问题,所以可以很轻鬆找到bug的位置

但也可以发现,重构此书很多地方都是以「重构」为前提来探讨,这代表此feature已经很确定了。

所以,对于功能不明确的prototype产品,是否要先写测试这就值得思考了,因为feature变动即测试也要修改,所以TDD也不是万灵丹,任何东西都不是银弹,我们必须适时且动态的做应对,才是一个好RD

后记

以前一直看不懂SOLID到底在说什么,而实际碰到之后才发现是一些很直观的东西,但我也能理解WIKI为什么要写得那么複杂,因为他必须把所有的情况都用两三句所诉说,我想后面的文章也可以依照我实务碰到的一些问题来描述SOLID,或许可以让大家更了解~


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章