一个测试工程师走进一家酒吧……

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯咖啡;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了 0.7 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了-1 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了 2^32 杯啤酒;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯洗脚水;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯蜥蜴;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一份 asdfQwer@24dg!&*(@;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,什么也没要;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了 NaN 杯 Null;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师冲进一家酒吧,要了 500T 啤酒咖啡洗脚水野猫狼牙棒奶茶;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师把酒吧拆了;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师化装成老板走进一家酒吧,要了 500 杯啤酒并且不付钱;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一万个测试工程师在酒吧门外呼啸而过;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧,要了一杯啤酒';DROP TABLE 酒吧;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试工程师们满意地离开了酒吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后一名顾客点了一份炒饭,酒吧炸了。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面是网上流行的一个关于测试的笑话,其主要核心思想是——你永远无法把所有问题都充分测试。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在软件工程中,测试是极其重要的一环,比重通常可以与编码相同,甚至大大超过。那么在 Golang 里,怎么样把测试写好,写正确?本文将对这个问题做一些简单的介绍。 当前文章将主要分两个部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Golang 测试的一些基本写法和工具"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何写“正确”的测试,这个部分虽然代码是用 golang 编写,但是其核心思想不限语言"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于篇幅问题,本文将不涉及性能测试,之后会另起一篇来谈。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"为什么要写测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们举个不太恰当的例子,测试也是代码,我们假定写代码时出现 bug 的概率是 p(0

0,t(i+1)为对于 t(i)的测试,t(i+1)正确为 t(i)正确的必要条件,那么对所有的 i,i>0,t(i)正确都是 t(0)正确的必要条件。。。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/59\/59926aa33715b243e0541d2e2b60c02d.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"测试的种类"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试的种类有非常多,我们这里只挑几个对一般开发者来说比较重要的测试,做简略的说明。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"白盒测试、黑盒测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是从测试方法上可以分为白盒测试和黑盒测试(当然还存在所谓的灰盒测试,这里不讨论)"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白盒测试 (White-box testing):白盒测试又称透明盒测试、结构测试等,软件测试的主要方法之一,也称结构测试、逻辑驱动测试或基于程序本身的测试。测试应用程序的内部结构或运作,而不是测试应用程序的功能。在白盒测试时,以编程语言的角度来设计测试案例。测试者输入数据验证数据流在程序中的流动路径,并确定适当的输出,类似测试电路中的节点。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"黑盒测试 (Black-box testing):黑盒测试,软件测试的主要方法之一,也可以称为功能测试、数据驱动测试或基于规格说明的测试。测试者不了解程序的内部情况,不需具备应用程序的代码、内部结构和编程语言的专门知识。只知道程序的输入、输出和系统的功能,这是从用户的角度针对软件界面、功能及外部结构进行测试,而不考虑程序内部逻辑结构。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们写的单元测试一般属于白盒测试,因为我们对测试对象的内部逻辑有着充分了解。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"单元测试、集成测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从测试的维度上,又可以分为单元测试和集成测试:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整合测试又称组装测试,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"单元测试可以是黑盒测试,集成测试亦可以是白盒测试"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"回归测试"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回归测试是软件测试的一种,旨在检验软件原有功能在修改后是否保持完整。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回归测试主要是希望维持软件的不变性,我们举一个例子来说明。例如我们发现软件在运行的过程中出现了问题,在 gitlab 上开启了一个 issue。之后我们并且定位到了问题,我们可以先写一个测试(测试的名称可以带上 issue 的 ID)来复现问题(该版本代码运行此测试结果失败)。之后我们修复问题后,再次运行测试,测试的结果应当成功。那么我们之后每次运行测试的时候,通过运行这个测试,可以保证同样的问题不会复现。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一个基本的测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们先来看一个 Golang 的代码:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add.go\npackage add\n\nfunc Add(a, b int) int {\n   return a + b\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个测试用例可以写成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在命令行我们使用 go test"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"go test\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个时候 go 会执行该目录下所有的以_test.go 为后缀中的测试,测试成功的话会有如下输出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\nPASS\nok      code.byted.org\/ek\/demo_test\/t01_basic\/correct       0.015s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设这个时候我们把 Add 函数修改成错误的实现"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add.go\npackage add\n\nfunc Add(a, b int) int {\n   return a - b\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再次执行测试命令"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\n--- FAIL: TestAddWrong (0.00s)\n    add_test.go:11: the result is -1 instead of 3\nFAIL\nexit status 1\nFAIL    code.byted.org\/ek\/demo_test\/t01_basic\/wrong 0.006s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"会发现测试失败。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"只执行一个测试文件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么如果我们想只测试这一个文件,输入"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"go test add_test.go\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"会发现命令行输出"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test add_test.go\n# command-line-arguments [command-line-arguments.test]\n.\/add_test.go:9:9: undefined: Add\nFAIL    command-line-arguments [build failed]\nFAIL\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这是因为我们没有附带测试对象的代码,修改测试后可以获得正确的输出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test add_test.go add.go\nok      command-line-arguments  0.007s\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"测试的几种书写方式"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"子测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常来说我们测试某个函数和方法,可能需要测试很多不同的 case 或者边际条件,例如我们为上面的 Add 函数写两个测试,可以写成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 0)\n   if res != 1 {\n      t.Errorf(\"the result is %d instead of 1\", res)\n   }\n}\n\nfunc TestAdd2(t *testing.T) {\n   res := Add(0, 1)\n   if res != 1 {\n      t.Errorf(\"the result is %d instead of 1\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试的结果:(使用-v 可以获得更多输出)"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test -v\n=== RUN   TestAdd\n--- PASS: TestAdd (0.00s)\n=== RUN   TestAdd2\n--- PASS: TestAdd2 (0.00s)\nPASS\nok      code.byted.org\/ek\/demo_test\/t02_subtest\/non_subtest     0.007s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一种写法是写成子测试的形式"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ add_test.go\npackage add\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   t.Run(\"test1\", func(t *testing.T) {\n      res := Add(1, 0)\n      if res != 1 {\n         t.Errorf(\"the result is %d instead of 1\", res)\n      }\n   })\n   t.Run(\"\", func(t *testing.T) {\n      res := Add(0, 1)\n      if res != 1 {\n         t.Errorf(\"the result is %d instead of 1\", res)\n      }\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"执行结果:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test -v\n=== RUN   TestAdd\n=== RUN   TestAdd\/test1\n=== RUN   TestAdd\/#00\n--- PASS: TestAdd (0.00s)\n    --- PASS: TestAdd\/test1 (0.00s)\n    --- PASS: TestAdd\/#00 (0.00s)\nPASS\nok      code.byted.org\/ek\/demo_test\/t02_subtest\/subtest 0.007s\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到输出中会将测试按照嵌套的结构分类,子测试的嵌套没有层数限制,如果不写测试名的话,会自动按照顺序给予序号作为其测试名(例如上面的#00)"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"对 IDE(Goland)友好的子测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有一种测试的写法是:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"tcList := map[string][]int{\n   \"t1\": {1, 2, 3},\n   \"t2\": {4, 5, 9},\n}\nfor name, tc := range tcList {\n   t.Run(name, func(t *testing.T) {\n      require.Equal(t, tc[2], Add(tc[0], tc[1]))\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看上去没什么问题,然而有一个缺点是,这个测试对 IDE 并不友好:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/79\/79c622c03b202088a1f8ebaba9a15706.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们无法在出错的时候对单个测试重新执行 所以推荐尽可能对每个 t.Run 都要独立书写,例如:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"f := func(a, b, exp int) func(t *testing.T) {\n   return func(t *testing.T) {\n      require.Equal(t, exp, Add(a, b))\n   }\n}\nt.Run(\"t1\", f(1, 2, 3))\nt.Run(\"t2\", f(4, 5, 9))"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/56\/569cad132db865846b91302381f547e5.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"测试分包"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们上面的 add.go 和 add_test.go 文件都处于同一个目录下,顶部的 package 名称都是 add,那么在写测试的过程中,也可以为测试启用与非测试文件不同的包名,例如我们现在将测试文件的包名改为 add_test:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add_test\n\nimport (\n   \"testing\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个时候执行 go test 会发现"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\n# code.byted.org\/ek\/demo_test\/t03_diffpkg_test [code.byted.org\/ek\/demo_test\/t03_diffpkg.test]\n.\/add_test.go:9:9: undefined: Add\nFAIL    code.byted.org\/ek\/demo_test\/t03_diffpkg [build failed]\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于包名变化了,我们无法再访问到 Add 函数,这个时候我们增加 import 即可:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage add_test\n\nimport (\n   \"testing\"\n\n   . \"code.byted.org\/ek\/demo_test\/t03_diffpkg\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   if res != 3 {\n      t.Errorf(\"the result is %d instead of 3\", res)\n   }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们使用上面的方式来导入包内的函数即可。 但使用了这种方式后,将无法访问包内未导出的函数(以小写开头的)。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"测试的工具库"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/stretchr\/testify"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以使用强大的 testify 来方便我们写测试 例如上面的测试我们可以用这个库写成:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \/\/ add_test.go\npackage correct\n\nimport (\n   \"testing\"\n\n   \"github.com\/stretchr\/testify\/require\"\n)\n\nfunc TestAdd(t *testing.T) {\n   res := Add(1, 2)\n   require.Equal(t, 3, res)\n\n   \/*\n    must := require.New(t)\n    res := Add(1, 2)\n    must.Equal(3, res)\n    *\/\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果执行失败,则会在命令行看到如下输出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"% go test\nok      code.byted.org\/ek\/demo_test\/t04_libraries\/testify\/correct       0.008s\n--- FAIL: TestAdd (0.00s)\n    add_test.go:12:\n                Error Trace:    add_test.go:12\n                Error:          Not equal:\n                                expected: 3\n                                actual  : -1\n                Test:           TestAdd\nFAIL\nFAIL    code.byted.org\/ek\/demo_test\/t04_libraries\/testify\/wrong 0.009s\nFAIL\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"库提供了格式化的错误详情(堆栈、错误值、期望值等)来方便我们调试。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/DATA-DOG\/go-sqlmock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于需要测试 sql 的地方可以使用 go-sqlmock 来测试"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优点:不需要依赖数据库"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:脱离了数据库的具体实现,所以需要写比较复杂的测试代码"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"github.com\/golang\/mock"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"强大的对 interface 的 mock 库,例如我们要测试函数 ioutil.ReadAll"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func ReadAll(r io.Reader) ([]byte, error)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们 mock 一个 io.Reader"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ package: 输出包名\n\/\/ destination: 输出文件\n\/\/ io: mock对象的包\n\/\/ Reader: mock对象的interface名\nmockgen -package gomock -destination mock_test.go io Reader\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以在目录下看到 mock_test.go 文件里,包含了一个 io.Reader 的 mock 实现 我们可以使用这个实现去测试 ioutil.Reader,例如"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"ctrl := gomock.NewController(t)\ndefer ctrl.Finish()\nm := NewMockReader(ctrl)\nm.EXPECT().Read(gomock.Any()).Return(0, errors.New(\"error\"))\n_, err := ioutil.ReadAll(m)\nrequire.Error(t, err)\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"net\/http\/httptest"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我们测试服务端代码的时候,会先启动服务,再启动测试。官方的 httptest 包给我们提供了一种方便地启动一个服务实例来测试的方法。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"其他"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其他一些测试工具可以前往 awesome-go#testing 查找"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/github.com\/avelino\/awesome-go#testing"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何写好测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介绍了测试的基本工具和写法,我们已经完成了“必先利其器”,下面我们将介绍如何“善其事”。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"并发测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在平时,大家写服务的时候,基本都必须考虑并发,我们使用 IDE 测试的时候,IDE 默认情况下并不会主动测试并发状态,那么如何保证我们写出来的代码是并发安全的? 我们来举个例子,比如我们有个计数器,作用就是计数。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"type Counter int32\n\nfunc (c *Counter) Incr() {\n   *c++\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很显然这个计数器在并发情况下是不安全的,那么我们如何写一个测试来做这个计数器的并发测试呢?"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"import (\n   \"sync\"\n   \"testing\"\n\n   \"github.com\/stretchr\/testify\/require\"\n)\n\nfunc TestA_Incr(t *testing.T) {\n   var a Counter\n   eg := sync.WaitGroup{}\n   count := 10\n   eg.Add(count)\n   for i := 0; i  0 && j > 0 {\n      i += j\n   }\n   return i\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的测试用例覆盖率达到了 100%,但是并没有测试到所有的分支"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := AddIfBothPositive(1, 2)\n   require.Equal(t, 3, res)\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"并没有处理异常\/边界条件"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func Divide(i, j int) int {\n   return i \/ j\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Divide 函数并没有处理除数为 0 的情况,而单元测试的覆盖率是 100%"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := Divide(6, 2)\n   require.Equal(t, 3, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的例子说明 100%的测试覆盖并不是真的“100%覆盖”了所有的代码运行情况。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"覆盖率的统计方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试覆盖率的统计方法一般是: 测试中执行到的代码行数 \/ 测试的代码的总行数 然而代码在实际运行中,每一行运行到的概率、出错的严重程度等等也是不同的,所以我们在追求高覆盖率的同时,不能迷信覆盖率。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"测试是不怕重复书写的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里的重复书写,可以一定程度上认为是“代码复用”的反义词。我们主要从下面的几方面来说。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重复书写类似的测试用例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试用例只要不是完全一致,那么即便是比较雷同的测试用例,我们都可以认为是有意义的,没有必要为了代码的精简特地删除,例如我们测试上面的 Add 函数"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   t.Run(\"fixed\", func(t *testing.T) {\n      res := Add(1, 2)\n      require.Equal(t, 3, res)\n   })\n   t.Run(\"random\", func(t *testing.T) {\n      a := rand.Int()\n      b := rand.Int()\n      res := Add(a, b)\n      require.Equal(t, a+b, res)\n   })\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虽然第二个测试看起来覆盖了第一个测试,但没有必要去特地删除第一个测试,越多的测试越能增加我们代码的可靠性。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重复书写(源)代码中的定义和逻辑"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如我们有一份代码"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package add\n\nconst Value = 3\n\nfunc AddInternalValue(a int) int {\n   return a + Value\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"测试为"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   res := AddInternalValue(1)\n   require.Equal(t, 1+Value, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看起来非常完美,但是如果某天内部变量 Value 的值被不小心改动了,那么这个测试无法反应出这个改动,也就无法及时发现这个错误了。如果我们写成"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"func TestAdd(t *testing.T) {\n   const value = 3\n   res := AddInternalValue(1)\n   require.Equal(t, 1+value, res)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就不用担心无法发现常量值的变化了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文转载自:字节跳动技术团队(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/9x8wdhDDMn-UBkTPG4SsCg","title":"xxx","type":null},"content":[{"type":"text","text":"一个测试工程师走进一家酒吧……"}]}]}]}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章