单元测试不是持续集成的基础 - fsword.github.io

关于软件测试,最近也在持续实践,对这篇《单元测试不是持续集成的基础》感同身受,作者李福,来自阿里技术质量部。

在我司实践单元测试中,有的单元测试刚写好,业务一发生变化就不得不完全抛弃,耽误时间。当然也有一种说法,单元测试代码的稳定是主要靠好的API设计:如何保持unit test代码的稳定。“API切实正确切割了需求,那么在重构的时候API就基本不用变化,unit test也不用重写。”关于这个我也是赞同的,然而说法过于理想化,难以实践落地,毕竟“好的API设计”这事还是依赖于人,可能第一个人可以,后边换了一个人设计API,整个项目就乌烟瘴气了。

说个题外话,也是因为成本的原因,个人比较倾向于BDD而非TDD。对于研发测试目前有这两点心得:

  • 测试的出发点不是找漏洞,而是对设计任务的验证
  • 测试的依据是文档,可执行的文档才是好文档

原文

很多人关注甚至想尝试持续集成,然而也有一些人担心团队缺乏基础——“我们连单元测试都做不好,做持续集成不太合适吧”。如果不会走就学跑,那确实容易摔跤。不过,单元测试和持续集成并不是走和跑的关系。

为避免误解,首先明确一下名词(虽然没有照抄书本,但是应该不会差太远吧):

  • 单元测试:以验证某个代码单元的正确性为目标进行的自动化测试活动,“代码单元”通常是函数、方法或者是类,测试过程中,目标单元对外部的编译或者功能依赖由stub或者mock技术进行隔离。
  • 持续集成:一种敏捷实践,重点是尽早进行系统的集成测试,它在狭义上包括部署自动化和测试自动化。“持续”一般被理解为不断的对研发变更进行整体验证,“集成”通常包括对分支的集成(因G此一般推荐单分支开发)和在一定条件下对不同子系统或者模块进行的集成。 可以看出,单元测试针对的目标是局部而非整体,而持续集成面对的是整体。按照“饭要一口一口吃”的老话,似乎应该先做单元测试。

然而单元测试并不是免费的。任何自动化测试都是基于测试目标的功能而实现,因此,测试目标的稳定性就变成了测试价值的一个重要因素——时常发生变化的代码,对其做自动化测试是不划算的。那么,单元测试所针对那些小粒度的类、方法和函数,它们变化剧烈吗?

这可能和软件系统的类型有关。例如,如果我们是在开发一个短信发送客户端,由于所遵循的SMGP协议本身是相对稳定的,网络相关的功能单元就是稳定的;然而,如果我们开发一个应用系统(比如各种大大小小的互联网应用),业务上的变化可能对下层的模型代码产生天翻覆地的影响。考虑到大部分的软件研发团队和研发工程师们所处理的都是基于数据库+web的应用系统,我们所遇到的场景很可能是后一种情况。

去年我所在的团队推进质量改进时我们就发现了这个规律,当时我们首先推进的就是单元测试,虽然我强调“自动化测试”而非单元测试,但是开发同事们都很自然把精力放到了单元测试上。在一段时间的热心实施以后,一些人开始出现不同的声音——“有些测试刚写好,业务就发生了变化,不得不完全抛弃,瞎耽误时间”。问题显然不是同事们不尽责,我们分析发现,因为单元测试用例过于关注细节,业务变化的情况下很难进行积累,再继续下去会出现“边际效益递减”的情况,而如果开始做持续集成方面的工作,则可以补充自动化的集成用例——它相对稳定。

除此之外,单元测试还有一个常见的问题:mock的代价。

几年前ThoughtWorks的李晓有过一篇《不要把Mock当作你的设计利器》这里还有gigix转述郭晓的观点——

I did have some doubts about using Mocks when i was programming, similar 
reasons - too hard to refactory, too brittle. And i total agree with the 
three places to use it - external resources (I/O), UI, third party API.

也许有人觉得这里的观点有些“极端”(好像中国人对“极端”是比较敏感的 :-D ),然而我们在实际工作中很容易感受到上述文章和引论所说的痛点。这里存在两个方面的问题——

  • 对变化不友好:一旦我们进行了mock,就在事实上建立了对外部变化的“屏障”——每次发生变化时都有可能忘记了被mock掉的“结合点”,即使记得,也增加了重构的成本,时间一长,维护mock代码就变成了一件苦差事
  • 推迟集成:有了mock以后,我们可以很容易就建立起自动化验证机制。但是错误往往在于疏忽——mock掉的那个东西,未来需要使用“真实的东西“再测一遍,这不止增加了测试成本,而且还会在前期给人以“系统没问题”的错觉 顺便说一句,这些问题在stub中也是类似的,mock和stub还有一些差异,但是这里就不涉及了。

对于这些分析,路宁同学 的一个简单易用的观点是:“不对自己开发的模块写mock”,这个很好理解,因为自己开发的模块可以直接用“真家伙”,那么“假李鬼”也就用不着了。

我们是否可以沿着这个思路继续推进呢?实际上,之所以要区分“自己的模块”,是因为“自己的模块”好合作(自己和自己当然好合作),那么在我们推进持续集成以后呢?

持续集成,表面上看是在做部署自动化和测试自动化。然而这个实践的一个重要价值是“弥合缺口”——通过持续的将版本控制系统的多个分支合并到一个分支上,避免了分之间的鸿沟越来越大;通过持续的将系统的各个部分完整的部署在一起进行自动化联调和系统测试,避免了子系统与子系统、模块与模块之间的衔接隔阂。

前者好理解,后者一般容易被忽视,我们知道,某些语言特性和框架也试图解决这种系统和模块边界 的衔接问题,例如java的interface,它就是设计来建立系统间协作接口的。然而真实的世界很难用 interface这类技术进行约束,即使实现了同样的interface,我们也不确定边界两边都遵守共同的约 束,能让我们放心的只有联调和系统测试。 显然,在我们推进持续集成的工作并通过这个工作不断的“弥合缺口”以后,那些之前不得不mock掉的所谓“别人的模块”甚至“外部的子系统”也就不再变得遥不可及而难以合作了。于是,我们惊喜的发现——mock变得可以省略了,随着持续集成的推进,一些原来不得不编写的mock可以直接用“真家伙”代替,而原来所倚仗的单元测试用例也随之变成了集成用例、联调用例……

所以,单元测试并不是集成测试的基础。实际上,往往是持续集成扩展了质量保障的手段和方式,并因此减弱了单元测试的压力,从此我们可以专注在必要的单元测试用例上了。

如果你的团队做单元测试不是很给力,可以先找找原因,如果不是大家的主观意愿问题,不妨和持续集成的工作一起推进吧

参考资料


Laravel 中 Lang get 方法的简单实现

Laravel 中获取语言的方法 Lang::get() 非常好用,我经常将一些错误提示放到这些文件里。比如:

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Authentication Language Lines
    |--------------------------------------------------------------------------
    |
    | The following language lines are used during authentication for various
    | messages that we need to display to the user. You are free to modify
    | these language lines according to your application's requirements.
    |
    */

    'failed' => '验证失败。',
    'throttle' => '登录验证过于频繁,请 :seconds 秒后再试。',
    'link_not_exist_or_expired' => '该链接不存在,或超时失效',
];

获取的方法则如下:

['100', Lang::get('auth.throttle', ['seconds' => $this->lockoutTime()])];

后面的数组将替换 :seconds 处。

期间自己某个功能也需要模板,但又不好使用Lang这个方法,遂自己实现了一个类似的方法。很简单的:

if (!function_exists('lang_get')) {
    function lang_get($str, array $params = [])
    {
        foreach ($params as $key => $value) {
            $str = str_replace(':' . $key, $value, $str);
        }
        return $str;
    }
}

Linux 设定静态IP地址

临时修改

ifconfig eth0 10.192.147.241 netmask 255.255.255.0
route add default gw 10.192.147.245

vi /etc/resolv.conf

nameserver 192.168.0.1

永久修改

vi /etc/network/interfaces

auto lo
iface lo inet loopback
auto eth0               # auto 开机自动连接网络 allow-hotplug 
# iface eth0 inet dhcp # 设置成DHCP,动态ip
iface eth0 inet static # static表示使用固定ip
address 192.168.038
netmask 255.255.255.0 # 子网掩码
gateway 192.168.0.1

vi /etc/resolvconf/resolv.conf.d/base

nameserver 8.8.8.8
nameserver 8.8.4.4

常见问题

  • stop: Job failed while stopping

    我使用 Ubuntu 14.4 版本时使用重启网络命名 service networking restart,显示这个错误。 解决的办法是

      ifdown --exclude=lo -a && ifup --exclude=lo -a
    
  • 在配置网络时auto与allow-hotplug的区别

    auto

      语法:
      auto <interface_name>
      含义:
      在系统启动的时候启动网络接口,无论网络接口有无连接(插入网线),如果该接口配置了DHCP,则无论有无网线,系统都会去执行DHCP,如果没有插入网线,则等该接口超时后才会继续。
    

    allow-hotplug

      语法:
      allow-hotplug <interface_name>
    
      含义:
      只有当内核从该接口检测到热插拔事件后才启动该接口。如果系统开机时该接口没有插入网线,则系统不会启动该接口,系统启动后,如果插入网线,系统会自动启动该接口。也就是将网络接口设置为热插拔模式。
    

参考资料


记录使用Docker时的一些问题

无法启动Docker服务

运行 service docker start 时报错

Redirecting to /bin/systemctl start docker.service Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

解决问题的办法就是清空 /var/lib/docker 下的所有文件。要注意这样会导致你所有的Docker数据都会丢失。

来自 [docker service failed to start #25913 github issues](https://github.com/docker/docker/issues/25913)

docker 主机的 iptables 设置

  • iptables failed - No chain/target/match by that name

    一个bug,重启dockers服务就好了。

      mv /var/lib/docker/network/files /tmp/docker-iptables-err
      systemctl restart docker
    
  • iptable filter

    使用 Docker PPTP 的 iptable 例子

      *nat 
      -A POSTROUTING -s 10.99.99.0/24 -o eth0 -j MASQUERADE
    
      * filter
      -A FORWARD -d 10.99.99.0/24 -j ACCEPT
      -A FORWARD -s 10.99.99.0/24 -j ACCEPT
    

    这个 issue 是我提的。提完之后自己就找到答案了。

来自iptable filter - github

can’t modprobe af_key in debian8

这个与主机提供商有关了。主机在使用 docker 时某些项目需要使用 IPsec NETKEY 内核模块,其它主机不了解,linode的默认内核是修改过的,所以没有这个内核模块。如果想启用的话需要在后台配置里将默认启动内核改为 GRUB 2 模式。

来自docker issue - github