各位 V 友们对 单元测试 有什么心得?

2016-12-14 09:58:17 +08:00
 jianzhiyao020

题主是搞(Pai)(Huang)(Pian)的,最近做单元测试的话,很容易就陷入一些思考,这样做对不对呢?所以就想到上来问问 V 友,借鉴学习。我个人比较强迫症,追求自己写的代码完美。

3481 次点击
所在节点    PHP
7 条回复
wujunze
2016-12-14 11:05:16 +08:00
PHPUnit 可以试试
jianzhiyao020
2016-12-14 11:47:54 +08:00
@wujunze PHPUnit 的话现在就是在尝试中,就是不知道自己写的测试用例如何合格
AbrahamGreyson
2016-12-14 12:17:27 +08:00
不要问泛泛而谈的问题。
jerray
2016-12-14 12:34:28 +08:00
我不知道对不对,反正写了单元测试以后我发现效率比以前高了不止一两倍。
有人说写了更多的代码。但是我想说的是,写点儿代码把手动验证的流程交给计算机来做,并且可以快速重复做很多遍,到底是省事儿了还是麻烦了自己琢磨。
feiyuanqiu
2016-12-14 13:46:48 +08:00
首先要明确你为什么要写单元测试?你希望它解决什么问题?回答了这个问题之后写测试的思路可能就比较清晰了

对于我来说,单元测试主要的作用是提高代码的可维护性,确保以后修改代码能及时发现影响范围,重构是否破坏了原有的业务逻辑?修改是否影响到了其他业务逻辑?如果程序写完之后就不需要维护了,那就完全没必要写测试

那么由此再说说我的一些体会

1. 测试代码要简洁清晰
测试代码比业务代码更追求可读性,它唯一的用户就是维护者,不需要考虑性能之类的问题,而混乱的测试代码比混乱的业务代码更让人不想维护。一个测试应该遵循三段论: 初始化场景,执行被测试方法,验证执行结果,所以一个理想的测试方法应该只有三行( clean code 书里已经说得很清楚了)

2. 测试范围
所有 public 方法,重要的 protected 方法,几乎不测试 private 方法
public 方法是类的公开 api ,可能被各种业务调用,所以一定要保证任何改变了 public 方法行为的修改都被检查到
同样 protected 方法是对子类公开的,对于被很多子类使用的 protected 方法,需要确保对它的修改不影响子类的行为
private 方法很不稳定,可能经常被修改,它只在类内部被调用,对 public, protected 方法的测试已经把它覆盖到了

3. 针对方法特性测试,而不是针对实现测试
可以把被测试代码当成一个黑盒,给它一个需求要求的输入,然后验证它是否返回了期待的输出。不需要去验证它在处理这个东西的过程中干了什么,没必要,因为具体实现可能会因为各种原因而修改,我们只要确保测试能检查到修改后的程序是否满足需求要求就行了
同时,又要保证测试覆盖了方法的所有的需求点,包括异常情况都需要写测试覆盖到。一个测试用例写一个测试方法
比如这个测试文件,这些测试方法都只在测一个 public 方法(不要吐槽方法命名...)


4. 其他
- PHPUnit 的 DataProvider 慎用,不是它不好,是它很容易就搞出一些特别难维护的测试代码,比如把正常流程和异常流程的测试都放到一个测试方法里,然后用 DataProvider 提供的参数来进入不同的测试流程,整个测试方法一大坨, DataProvider 里面的数据又是一大坨,看得人想死
- 很多时候写测试会反向地要求你优化你的程序设计,最常见的例子就是依赖注入了,这里是之前 google 的一个 guide , http://misko.hevery.com/attachments/Guide-Writing%20Testable%20Code.pdf
vincenttone
2016-12-15 13:31:16 +08:00
我感觉楼上的都是复制粘贴的,没必要看。
DataProvider 该用还是可以用的,个人觉得很好用,数据容易维护。
单元测试用什么写,怎么写不重要,既然是白盒测试,衡量标准自然是代码覆盖率,这个覆盖率你应该有个预期,覆盖核心代码还是全部代码。
至于实施上,看你是设计驱动的还是开发驱动的,自己调节。
不要管那么多没什么用的,关键在于你写单测的态度和出发点是什么。
有上来问的时间,足够你写好多单测了。
feiyuanqiu
2016-12-15 15:57:05 +08:00
我在 v 站的回复还真没怎么复制粘贴过别人的东西。昨天吃饭的时候看到这个问题,用手机打了一点字,吃完回来在电脑上编辑了一下,删掉了附图,因为是项目的测试代码不应该发出来

我的回复里一直在强调测试代码的可维护性,因为这是切身的体会,我们团队早期对单元测试也不熟悉,照着文档就开写,追求 100%的覆盖率,每个 private 方法都用反射搞出来把测试写了,这样做了之后覆盖率报告 x 是很好看了,全是绿色,但是到了之后需求来了要修改代码的时候就很难过了,不仅要改业务代码,还要修改大量的测试代码,这对开发来说是个很大的负担,并且由于每个测试代码写得也很庞杂,导致理解和维护这些测试代码变得比维护业务代码还麻烦,进而就导致了开发人员消极对待单元测试

因为我们项目在上升期,每期迭代都有很多需求要做,也发生过新需求的修改影响到别的业务的正常使用的问题。所以我们团队对于单元测试最大的期望就是能及时发现新的代码修改对整个系统的影响,同时尽量减少开发人员的负担,所以我们现在基本只测试 public 方法,虽然如此,因为类写得比较内聚,对 public 方法完善的测试其实也已经覆盖到了大部分代码

关于 DataProvider ,为什么我烦它呢?也是在早期,大家基本上就是按照一个业务方法对应一个测试方法这样来写,对于复杂的业务方法,有很多异常情况处理,这些都需要测试,于是就在 DataProvider 里面弄上十几条各种数据,然后在一个测试方法里面对所有这些场景编测试代码,一堆的 if ,等于是把十几种测试用例一股脑地揉在一个方法里面,你可以想见这种测试代码在维护的时候是什么感觉。所以之后我们一般就按照一个测试用例一个测试方法这样来组织测试,很少用 DataProvider 了

关于为什么追求测试代码的简洁性,因为我改过几个之前我自己写的测试,这些测试方法或者是依赖于一些外部接口,或者是会调用一些重型操作,我们测试的时候是通过向被测试方法注入 MockObjectBuilder 构造出来的 Mock 对象来做的,于是出现了一个测试方法前面几十行都在构造测试环境,后面几十行各种 assert 检查数据细节是否达到了要求,很不好看,一眼看不出测试,所以基本上现在测试代码就写成这个样子:

/**
* 成功调用 add 方法后,应该持久化消息数据
*
* @return void
*/
public function testShouldPersistMessage_afterAdd()
{
$this->assertMessageNotExist($this->message);

$this->service->add($this->message, new SampleDataProvider());

$this->assertMessageExist($this->message);
}



所以上一条的回复都是我近些日子里实际的一些体会,不一定对,也不适合所有人,权且当作一个参考吧

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/327498

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX