代码测试意味着完全消灭了Bug?
日前,一位名为 Jens Neuse 的开发者在改进其 graphql 解析库的过程中,发现词法分析器和解析器中存在很多的低效率,因此不得不重构完整的代码库(https://medium.com/@jens.neuse/want-to-write-good-unit-tests-in-go-dont-panic-or-should-you-ba3eb5bf4f51)。在重构的过程中,Jens Neuse 认为测试至关重要。然而,本文作者却并不这么想,他认为测试并不意味着一切,接下本文将以 Go 语言为例,分析其原因。
作者 | martin
译者 | 梁蕊
来源 | CSDN(ID:CSDNNews)
我使用过的一些最难用的代码是“易于测试”的代码。代码将所有内容抽象到开发者难以想象发生了什么的程度,只是为了向原本非常简单的函数中添加“单元测试”。DHH 称这种为测试引起的设计损坏。
测试只是确保用户的程序正常运行的工具之一。另外一种非常重要的工具是以一种易于理解和推理(简单)的方式编写代码。
在此,推荐开发者可以查阅一本使用广泛的测试书籍,Robert C.Martin 编写的《Clean Code》,其中部分内容是为了响应更复杂的代码而写的,在这些程序中,你阅读了 1000 行代码,但仍然不知道发生了什么。我最近不得不将一个简单的 Java “表情符号替代品”(:joy:→?)移植到 Go。为了确保兼容性,我查看了它的实现类。这包含了一大堆类、工厂、以及所有这些只会导致在字符串上调用 regexp 的东西。
在像 Ruby 和 Python 这样的动态语言中,测试对于不同的前提很重要,就像下面这段代码将会正常工作:
if condition:
print('w00t')
else:
nonexistent_function()
当然,除了如果进入 else 分支,很容易会拼写错误的东西或者混合东西。
在 Go 语言中,这些问题都不那么令人担忧。Go 有一个静态类型系统,重点是可以编写简单直接的代码,易于理解。即使对于许多动态语言,也有可选的输入系统(Python 中的函数注释,JavaScript 的 TypeSript)。
有时你可以做一个简单的实现,而不牺牲任何可测试性;太棒了!但是有时你必须找到一个平衡点。对于某些代码,不添加单元测试是可以的。
对“单元测试”的过分关注可能会对代码库造成难以置信的损害。有些代码库有大量的单元测试,这使得任何更改都非常耗时,因为你要为哪怕是很小的更改而修复一大堆测试。很多时候,这些测试都是重复的;像简单的 CRUD,HTTP 端点的每一层添加一个测试是一个常见的示例。在许多应用程序中,只依赖一个集成测试就可以了。
像 SQL 模拟这样的东西是另一个很好的例子。它使代码更复杂,更难更改,所以可以说我们添加了一个“单元测试” select * from foo where x = ?。最糟糕的是,除了验证你没有错误的查询 SQL 查询之外,它甚至不测试任何其他内容。一旦测试开始做任何有用的事情,例如验证它实际上从数据库中返回正确的行,单元测试纯粹主义者开始抱怨它并不是真正的单元测试,你做错了。
对于大多数查询,集成测试和/或手动测试都是很好的,并且广泛的 SQL 模拟充其量是多余的,并且在最坏的情况下是有害的。
当然也有例外:如果你有很多的 if cond {q += “more sql”} 那么添加 SQL 模拟来验证逻辑的正确性可能是一个好主意。即使在那些情况下,”非单元的单元测试(例如,仅访问数据库的那个)仍然是可行的选择。集成测试也是一种选择。很多应用程序无论如何都没有那种复杂的查询。
关注单元测试的一个重要原因是确保测试代码能够快速运行。这是对需要一天运行的大规模测试工具的响应。这在 Go 中也不是一个真正的问题。我编写的所有集成测试都在合理的时间内运行(最多几秒,通常更快)。GO 1.10 中引入的测试缓存使它不再受关注。
我所经历的故事
去年,一位同事重构了我们基于 ETag 的缓存库。旧代码非常直接且易于理解,虽然我没有声称它一定没有 Bug,但它确实在很长一段时间内都运行良好。
它应该已经在适当的地方写了一些测试,但它没有(我没有写原始版本)。请注意,代码并非完全没有经过测试,因为我们确实进行了集成测试。
重构的版本要复杂得多。除了花了两周时间将一段工作代码重构成另一段工作代码(另一篇文章的主题)之外,我并不相信它实际上要好得多。我认为自己是一位有一定造诣且经验丰富的程序员,在 Go 中拥有合理的知识和经验。总的来说,根据同行和绩效评估的反馈,我至少是“平均”技能水平的程序员,如果不是更多的话。
如果一个普通的程序员因为有很多层的抽象而难以理解一些简单的函数的本质,那么一定是出现了问题。重构提供了一个工具用另一个测试用例来验证正确性(简单性)。简单性很难保证正确性,但单元测试也不是。理想情况下,我们应该两点都做到。
后记:重构引入了一个 Bug 并删除了一个有用的功能,但现在更难添加,至少因为代码要复杂得多。
测试驱动开发
所有单元正常工作都不能保证程序正常工作。很多逻辑错误都不会被捕获,因为逻辑由几个单元一起工作组成。所以你需要集成测试,如果集成测试重复了一半的单元测试,那么为什么还要为这些单元测试烦恼呢?
测试驱动开发(TDD)也只是一种工具。它可以很好的解决一些问题; 对其他人而言并非如此。特别是,我认为“被迫在小单元编写代码” 在某些情况下会非常有害。有些代码只是一个串行脚本,上面写着“执行此操作,然后执行此操作,然后执行此操作”。在一大堆“小单元”中拆分它可以大大减少代码理解的容易程度,因此更难以验证它是否正确。
我必须修复一些 Ruby 代码,其中所有东西都是小单元。在 Ruby 社区中有一种强大的 TDD 文化,尽管单元很容易理解,但我发现理解应用程序逻辑非常困难。如果所有内容都以“小单位”分割,那么理解所有内容如何组合在一起以创建一个有用的实际程序将会更加困难。
你可以看到旧微内核与单片内核争论相同的摩擦,或者更近期的微服务与单片应用程序之间的摩擦。在原则上把所有东西分成一个个小的部分听起来像一个伟大的想法,但在实践中事实证明,使所有的小零件一起工作是一个非常困难的问题。混合方法似乎最适合内核和应用程序设计,平衡两种方法的优点和缺点。我认为这同样适用于代码。
需要澄清的是,我并不是反对单元测试或 TDD,并且声称我们所有人都应该按照生活中的方式编写代码。我编写单元测试并在有意义的时候实践 TDD。我的观点是,单元测试和 TDD 不是最后一个问题的解决方案,他们不应该不加区别的使用。这就是为什么我频繁的使用诸如“some”和“often”之类的单词。
测试框架
这让我想到了测试框架的主题。我从来没有理解像 goblin 这样的库正在解决什么问题。这怎么样:
Expect(err).To(nil)
Expect(out).To(test.wantOut)
对此有所改进?
if err != nil {
t.Fatal(err)
}
if out != tt.want {
t.Errorf("out: %q\nwant: %q", out, tt.want)
}
if 和==怎么了?为什么我们需要抽象呢?请注意,对于表驱动的测试中,您只需键入一次这些检查,因此您只需在此处保存几行。
Ginkgo 更糟糕。它变成了一个非常简单,直接且易于理解的代码片段,并且不仅仅是抽象的if,它还可以在几个不同的函数中完成执行(BeforeEach()和 DescribeTable())。
这称为行为驱动开发(BDD)。我不完全确定如何看待 BDD。我持怀疑态度,但我从来没有在一个大型项目中正确使用它,所以我犹豫不决是否放弃他。请注意,我说“正确”:大多数项目并不真正使用 BDD,他们只是使用带有 BDD 语法的库,并将其测试代码插入其中。那是特别的 BDD,或者说是伪 BDD。
无论 BDD 有什么优点,由于你的测试代码类似于 BDD 风格的语法,所以这些优点都不会显现出来。这本身就证明了 BDD 对许多项目来说可能不是一个好主意。
我认为这些 BDD(-ish)测试工具存在实际问题,因为它们混淆了你实际做的事情。无论如何,测试仍然是获取函数的输出并检查它是否符合你的预期。没有任何测试方法会改变这种基本原理。你添加的层越多,调试就越困难。
在确定某样东西是否“容易”时,我最关心的不是编写该东西是多么容易,而是当事情失败时调试是多么容易。如果这样可以让事情变得更容易调试,那么我很乐意花更多的精力写一些东西。
所有代码(包括测试代码)都可能以令人困惑,令人惊讶和意外的方式(“错误”)失败,然后你需要调试该代码。代码越复杂,调试起来就越困难。
程序员应该期望所有代码(包括测试代码)都要经历几个调试周期。请注意,对于调试周期,我并不是说“你需要修复的代码中存在错误”,而是“我需要查看此代码来修复错误”。
一般来说,我已经发现测试代码比常规代码更难调试,因为“代码表面”往往更大。开发者需要考虑测试代码和实际实现代码。而不仅仅是考虑实现代码。
添加这些抽象意味着你现在也必须考虑这一点!如果抽象会减少你必须考虑的范围,这可能是可以的,这是在常规代码中添加抽象的常见原因,但事实并非如此。它只是增加了更多需要考虑的东西。
所以这些都是错误的抽象:它们包装和混淆,而不是分离关注点并缩小范围。
关于开源项目
如果你有兴趣在开源项目中请求其他人来贡献,那么测试可以理解是一个非常重要的问题。
看到 PRs 上写着“这是代码,它可以工作,但我无法弄清楚测试,请暂停!”这并不罕见; 而且我很确定至少有几个人甚至从不打算提交 PR 只是因为他们被困在测试中。我知道我有。
有一个开源项目是我贡献的,我也想为之贡献更多,但是我没有,因为编写和运行测试太难了。每一个变化都是“在 15 分钟内编写工作代码,花 45 分钟处理测试”。这一点儿也不好玩。
结语
编写好的软件真的很难。当前我有一些关于如何实现好的软件的想法,但没有完整的实施方案。我知道“总是添加单元测试”和“总是使用 TDD”不是答案,尽管它们是有用的概念。打个比方:大多数人会同意自由市场是一个好主意,但与此同时,即使大多数自由主义者同意,但这并不是解决所有问题的完整方案。
原文:https://arp242.net/weblog/testing.html
(本文为AI科技大本营转载文章,转载请联系作者。)
征稿推荐阅读:
Windows 95被做成了一款软件,可玩扫雷和纸牌
给Chrome“捉虫”16000个,Google开源bug自检工具
2019全球AI 100强,中国占独角兽半壁江山,但忧患暗存
“百练”成钢:NumPy 100练
赵本山:我的时代还没有结束 | Python告诉你
“不厚道”的程序员:年后第一天上班就提辞职
Facebook神秘区块链部门首次收购,开放这些职位,你的技能符合吗?
微信帝国进化史:一个通讯工具如何在八年内制霸互联网?
写给程序员的裁员防身指南
这4门AI网课极具人气,逆天好评!(附代码+答疑)
点击“阅读原文”,打开CSDN APP 阅读更贴心!
相关文章:

Android系列讲座(2):为TextView组件加上边框
本文为原创,如需转载,请注明作者和出处,谢谢! 源代码Android系统本身提供的TextView组件并不支持边框,但可以对TextView进行扩展来添加边框。我们可以使用如下两种方法为TextView组件添加边框。 1. 编写一个继承TextV…

如何用Python做三阶拼图?
作者 | Ahab来源 | Ahab杂货铺(ID:AhabBin)今天是年假的最后一天,明天大家就要回到各自的工作岗位上去,公众号从今天开始恢复更新。不知道大家假期过得怎么样,帅张提到假期多关注一些家里的变化,自己也记录…

【C语言】libiniparser库使用例子,
libiniparser是C语言实现的ini文件解析库 使用实例如下 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h>#include "iniparser.h"void create_example_ini_file(void); int parse_ini_file(char * ini_nam…

laravel5.4 关于数据填充的知识
需求:大量excel表格数据 集中整理到一个规定数据表中,并且增加新字段做标记步骤:把需要整理的excel表格提前存放到mysql数据库指定的表中 ,可以用图形化工具来执行! 核心:利用laravel5.4 框架自带的填充功能…

windows 域环境应用---VNC远程协助工具的使用
域环境中,少不了远程协助工具,这可以替管理员省掉许多跑腿的事。<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />今天我就介绍一下远程协助工具中的一款real VNC.此工具有三个软件,vnc server…

从技术角度分析“抢票软件的加速”有多快?
作者 | 上海小胖来源 | Python专栏(ID:xpchuiit)大部分人在昨天前天已经回程了,不知道大家什么状态。我有2个状态并存:1.快点上班。2.继续休息。为什么会有这2种状态呢,还得从娃说起。昨天月嫂回家一天&am…

【物联网】NB-IoT简介
一、“我”是谁? NB-IoT:基于蜂窝的窄带物联网(Narrow Band Internet of Things, NB-IoT)成为万物互联网络的一个重要分支。NB-IoT构建于蜂窝网络,只消耗大约180KHz的带宽,可直接部署于GSM网络、UMTS网络或…

CES上百度无人车队炫技,陆奇要用“China speed”改变世界
本文由 「AI前线」原创,原文链接:CES上百度无人车队炫技,陆奇要用“China speed”改变世界编辑|EmilyAI 前线导读:“美国当地时间 1 月 8 日,百度在拉斯维加斯举办了主题为“AI is Changing the World, Chi…

《Microsoft Sql server 2008 Internals》读书笔记--第九章Plan Caching and Recompilation(10)
《Microsoft Sql server 2008 Internals》读书笔记订阅地址: http://www.cnblogs.com/downmoon/category/230397.html/rss 《Microsoft Sql server 2008 Internals》索引目录: 《Microsoft Sql server 2008 Internal》读书笔记--目录索引 上文主要介…

小心!你的脸正在成为色情片主角……
编译丨福尔摩望本文经授权转载自 猎云网(ID:ilieyun)【导语】那的确是她的脸,但是在她不知情的情况下被无缝地移植到了别人的身体上。网络上曾出现过这样的视频:一名女子穿着粉色肩膊上衣,坐在床上…

【modbus】libmodbus库的移植与使用
一、源码下载 1、libmodbus官网 http://libmodbus.org/download/ 2、选择长期稳定版本libmodbus-3.0.6.tar.gz 下载链接:http://libmodbus.org/releases/libmodbus-3.0.6.tar.gz 3、MD5值 c80f88b6ca19cabc4ceffc195ca07771 libmodbus-3.0.6.tar.gz …

IntelliJ IDEA控制台输出中文乱码问题解决
如果还不行,那么再极端的设置,在IDEA启动的时候强制设置为UTF-8: 打开增加-Dfile.encodingUTF-8,重启Intellij IDEA 再或者直接在项目运行的时候加入UTF-8的设置 如果还是不行,那么你可能装了一个假的IDEA。

【数据库】mysql移植
一、源码下载 1、下载mysql源码 源码下载地址:选择版本:5.1.72(这是个老版本,高版本需要使用cmake) https://cdn.mysql.com/archives/mysql-5.1/mysql-5.1.72.tar.gz 历史版本下载地址 源码官网地址,在Oper…

HTML添加上传图片并进行预览
使用说明:新建文件,直接复制粘贴,保存文件为html 格式,在浏览器运行即可; 第一种: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loos…

滴滴裁员2000多人,去年亏损超百亿
作者 | 非主流出品 | AI科技大本营(ID:rgznai100)据多家媒体的消息,在今天(1 月 15 日)上午的月度全员会上,滴滴 CEO 程维宣布公司将做好过冬准备,2019年会聚焦当前最重要的出行主业,…

自己写的Treeview控件绑定数据源
首先在数据库中建表,表的格式如下id name parent_id代码如下:/// <summary>/// 添加根节点/// </summary>/// <param name"list">表的所有数据orm的List集合,object为表对应的实体对象</param>private…

【GDB】gdbserver的使用,远程调试开发板
0、将gdbserver拷贝到目标板子上 注:gdbserver在交叉编译工具目录/debug-root/usr/bin下 可用file命令查看 ------------------------------------------------------------ $ file gdbserver gdbserver: ELF 32-bit LSB executable, ARM, version 1 (SYSV), statically linke…

关于javascript代码优化的8点建议
前面的话 本文将详细介绍JS编程风格的几个要点 松耦合 当修改一个组件而不需要更改其他组件时,就做到了松耦合 1、将JS从CSS中抽离:不要使用CSS表达式 //不好的做法 .box{width: expression(document.body.offsetWidth ’px)} 2、将CSS从JS中抽离&#…

英伟达Q4净利同比降49%,还能继续躺赚吗?
图片来自英伟达官网整理 | 非主流出品 | AI科技大本营(ID:rgznai100)北京时间 2 月 15 日凌晨消息,英伟达公布了 2019 财年第四季度及全年财报。报告显示,英伟达第四季度营收为 22.05 亿美元,与上年同期的 29.11 亿美元…

管理磁盘和文件系统
管理磁盘和文件系统 实验案例一:迁移/home分区并设置磁盘配额 实验环境 公司原来安装的rhel5服务器采用了自动分区的方案,随着使用服务器的系统用户数量不断增多,根分区经常面临磁盘空间耗尽的情况,偶尔有几次还导致系统无法启动。…

TIOBE 2月编程语言排行榜:Python逼近C,Groovy重回TOP 20
作者 | 唐小引出品 | CSDN(ID:CSDNnews)新月伊始,一月一更的 TIOBE 编程语言社区发布了最新的 2 月编程语言排行榜。继 Python 语言成为 "2018年度编程语言" 的称号,在2月的编程语言排行榜中,Pyt…

【视频】YUV基础
参考网站:https://msdn.microsoft.com/en-us/library/windows/desktop/dd206750(v=vs.85).aspx YUV定义: YUV颜色空间主要用于优化彩色视频信号的传输,并使其向后兼容老式黑白电视。 其中“Y”表示明亮度(Luminance或Luma),也就是灰阶值; 而“U”和“V” 表示的则是色…

删除电脑里的空文件夹
发现电脑好多空文件夹,网上没有找到删除空文件夹的程序,自己动手谢了段代码 import java.io.File;/*** author zhangyanan*/ public class FileUtil { public static void main(String args[]) {String[] disk{"C:/","D:/","E:…

QT创建子对话框
1)根据UI文件动态加载对话框QUiLoader uiLoader;QFile file("://new/prefix1/testDailog.ui");QWidget *myWidget uiLoader.load(&file);myWidget->show();2) 创建非模态对话框 (需建立对话框类testDialog)testDialog *dialog new testDialog(this);dialog-…

对标Bert?刷屏的GPT 2.0意味着什么
作者 | 张俊林,中国中文信息学会理事,中科院软件所博士。目前在新浪微博 AI Lab 担任资深算法专家。在此之前,张俊林曾经在阿里巴巴任资深技术专家并负责新技术团队,以及在百度和用友担任技术经理及技术总监等职务。同时他是技术书…

Wedge 100-32X 100GbE Data Center Switch
1、总体设计 (https://www.edge-core.com/productsList.php?cls1&cls25&cls367) 基于 Facebook’s Wedge 100 design. 30个QSFP28光模块 使用broadcom Tomahawk 3.2 Tbps 芯片 COM-E CPU module,Intel Atom E3800 x86 processor 支…

【视频】对RTSP抓包,分析通讯流程
1、C–>S(客户端向服务端):OPTIONS命令查询服务器提供的方法 Request: OPTIONS rtsp://192.168.1.10:554/12 RTSP/1.0\r\n CSeq: 2\r\n User-Agent: LibVLC/2.2.8 (LIVE555 Streaming Media v2016.02.22)\r\n \r\n 2、S–>C:回应OPTIONS Response: RTSP/1.0 200 OK\…

JDBC知识全攻略
1. JDBC 的用途是什么?简单地说,JDBC 可做三件事:与数据库建立连接,发送SQL 语句,处理结果。下列代码段给出了以上三步的基本示例:Connection con DriverManager.getConnection ("jdbc:odbc:wombat&…

一次性掌握机器学习基础知识脉络 | 公开课笔记
来源 | AI科技大本营在线公开课 嘉宾 | 张相於 整理 | suiling 本次公开课AI科技大本营邀请到了阿里巴巴的高级算法专家张相於,他将从数据的概率分布开始介绍机器学习核心概念之间的有机关系,帮助大家建立知识脉络,做到知识的有机吸收。同时&…