进化的测试

关注软件测试,白盒测试,自动化测试,性能测试

代码中的注释

我第一个实习的公司,是一个美资公司,在印度设立研发中心可能都已经有超过10年的经验了,那时候一些前辈们告诉我,印度人写的代码可能不如中国人那么聪明,但是他们的注释实在是非常详细,有时候甚至达到了1:1的比例,试想想,100行代码就有100行注释,这是多么的恐怖啊。

经常听到会有人抱怨道,怎么这段代码没有注释啊,这是为什么这样写的啊,如此云云。仿佛没有注释,这个世界就不转了。类似的事情也经常发生在QA们的身边,只不过注释换成了文档。

刚毕业的时候做白盒测试,现在回想起来,那时候的我测试的代码大部分都是不包含注释的,不过我测试起来并没有太大的困难,总结一下,应该有以下几点原因:

  1. 有意义的函数名、变量名。函数的命名让人一看就大概知道在做什么,例如PostBlog就是发布一篇博客,如果遇到一个叫SaveProfile的方法但做的却是加好友,那我想再多的注释我也会头晕
  2. 代码不会说谎。根据经验,如果一段代码理解起来很费劲,那么通常里面都会隐藏着问题。代码就是最好的注释,一些过时的注释,设置会对阅读代码的人产生误导
  3. 充分的沟通。虽然游走于几个项目组,跟不同的开发人员打交道,但是每当遇到问题的时候总会主动跟相关的人沟通,一个活生生的人坐在那里不问,却迷信什么文档,这真是本末倒置

注释,能不写就别写,实在要写,写WHY而不是WHAT。

联系一下最近在Team内写的一个新的回归测试工具,里面基本没有注释,希望过几个月以后,自己还能够看看代码就知道当时那段代码为什么这样写。

DSL与自动化测试 – 用Python实现简单的DSL

自动化测试,一个在测试领域中被广为熟知,也是被谈论最多的概念之一。DSL (Domain Specific Language),一种高度抽象,用于某个特定领域下编程语言。软件测试在大多数情况下都是对某个特定行业的软件系统进行测试,所以这两者应该可以很好的结合起来,事实上也是这样的,QTP里面的keyword view,其实就是DSL的一个实现。DSL一般可以分为两个大的类型,分别是External DSL 和 Internal DSL (引用自Martin Fowler)。External DSL 一般来说是跟其实现语言不一样的 DSL,常见的External DSL 有:SQL和XML配置文件;而Internal DSL 一般来说就是该DSL使用某个现成的编程语言(就是所谓的host language),然后对host language进行一些改造而成。

我们在测试中会遇到很多问题,其中一些问题,几乎是所有公司所有团队都会遇到的,例如测试覆盖率不够,测试的时间不够等等。面对这些问题,自动化测试自然而然地成为解决这些问题的首选方法。但是自动化测试真的就是银弹麽?不见得!以前曾经在ASP.NET QA 的博客中给他们留言,请教过关于自动化测试的事情,我记得其中有一个回复是说,在某个release中过度地使用自动化测试,一切东西都想实现自动化测试,而忽略了产品本身的功能、特性的关注,结果就是超高的自动化测试覆盖率,但是很差的产品质量。大家都去实现自动化测试了,谁来做功能点的覆盖呢?某些领域的专家(SME),他们可能对测试技术是一无所知的,要把这些领域专家和测试实施结合起来,DSL就是一个比较好的桥梁。

我在工作中遇到的问题是,我需要测试一个类似UV(独立用户访问数)统计的系统,统计UV的方法其实就是根据_uid cookie的值来判断这个用户在某段时间内访问过我们的系统多少次,访问了哪些站点,进行了什么样的行为。其中有2个地方比较麻烦,第一就是在测试过程中要不断地拷贝cookie,这样拷来拷去两三次以后很容易就混乱,出错;第二就是需要记录访问哪些站点,这些站点都只是ID,也是需要不断地修改请求,测试时间长了也是很容易出错。所以我就打算在原来的测试工具基础上,实现一个简单的Internal DSL。先看成品:

@tc
def uniq_inventory_case01():
    test= testTool()
    test.user('a').view('asset55100002').anetwork('55100').onsite('site55100503').snetwork('55100').dnetwork('55100').times(1).go()
    test.user('b').view('asset55100002').anetwork('55100').onsite('site55100503').snetwork('55100').dnetwork('55100').times(2).go()
    test.user('b').view('asset55100002').anetwork('55100').onsite('site55100504_noad').snetwork('55100').dnetwork('55100').times(4).go()

实例化一个testTool对象,然后就是指定哪个用户:user(‘a’)或者user(‘b’),看的视频的ID:view(‘asset55100002′),这个视频属于哪个CRO呢?anetwork(’55100′);放在哪个网站呢?onsite(‘site55100503′);网站是谁的呢?snetwork(’55100′);谁是分发者呢?dnetwork(’55100′);看了多少次呢?times(4);最后一个有点儿丑陋的go()。

像这样子一句话里面N个方法连着用,就叫Method Chaining,Method Chaining通常可以让代码变得更加人性化,读起来更加容易。但是使用Method Chaining通常会遇到一个问题,就是很难判断就是到了哪个方法才是终结呢?是不是有些方法的调用是可选的,有些是必选的呢?其中一个解决方法就是我用到的,放一个.go()方法在最后,作为终结方法。要实现Method Chaining,其实只需要顶一个类,对于需要做连接的方法,最后都返回这个类的实例。例如:

def view(self, assetid):
    if assetid:     self.asset_id = assetid
    return self
 
def anetwork(self, networkid):
    if networkid:   self.a_network_id = networkid
    return self
 
def snetwork(self, networkid):
    if networkid:   self.s_network_id = networkid
    return self
 
def dnetwork(self, networkid):
    if networkid:   self.d_network_id = networkid
    return self
 
def onsite(self, sectionid):
    if sectionid:   self.site_section_id = sectionid
    return self
 
def times(self, times):
    if times>0:       self.request_times = times
    return self

最后一个终结方法go(),就做真正的处理

def go(self):
    if self.asset_id and self.site_section_id and self.times and self.a_network_id and self.s_network_id:
        self.prepareRequest()
        for i in range(self.request_times):
            self.sendRequest()
 
        self.cleanup()
    else:
        info = 'Required information missing, abort running.'
        logging.debug(info)
        print info

如果是实现一个External DSL 的话,的确难度不小;但是Internal DSL其实并不是很高深,也不是很难实现,在它的帮助下,可以把工作完成的更好,对自己以后维护测试用例也带来了不少方便。

TestLink不能连接BugZilla的解决办法

TestLink是一个基于Web的PHP开源测试管理系统,虽然用起来跟QC那些商业软件比起来不是那么爽,但是由于是开源、免费,所以越来越多的公司在用TestLink。BugZilla作为老牌的bug管理工具,同样有着很大的用户群。

TestLink有一个BugTracking的接口模块,可以使得TestLink可以与其他BugTracking系统集成。在集成的过程中发现TestLink提示错误:Bug ID does not exist on BTS(中文的话是’bug的ID在BTS中不存在!’),找到TestLink的代码文件bugAdd.php,找到下面这段代码块

if($args->bug_id != "")
{
	$msg = lang_get("error_wrong_BugID_format");
	if ($g_bugInterface->checkBugID($args->bug_id))
	{
		$msg = lang_get("error_bug_does_not_exist_on_bts");
		// 问题在这里
		if ($g_bugInterface->checkBugID_existence($args->bug_id))
		{ 	  
			if (write_execution_bug($db,$args->exec_id, $args->bug_id))
			{
				$msg = lang_get("bug_added");
				logAuditEvent(TLS("audit_executionbug_added",$args->bug_id),"CREATE",$args->exec_id,"executions");
			}
		}
	}
}

问题出在$g_bugInterface->checkBugID_existence($args->bug_id)这个方法中。在、TestLink的int_bugzilla.php文件中,并没有overload这个checkBugID_existence的方法,所以这个方法就会按照int_bugtracking.php中的默认实现,返回false。TestLink就会出现error_bug_does_not_exist_on_bts这个ERROR

解决这个问题很简单,就是在int_bugzilla.php中自己实现checkBugID_existence方法,简单的实现如下:

function checkBugID_existence($id)
{
	$status_ok = 0;
        //关键是下面这个Query bug id的语句,大家自己看看数据库是哪个表,根据实际情况自己修改	
	$query = "SELECT bug_id FROM bugs WHERE bug_id='" . $id ."'";
	$result = $this->dbConnection->exec_query($query);
	if ($result && ($this->dbConnection->num_rows($result) == 1))
	{
		$status_ok = 1;    
	}
	return $status_ok;
}

OK。在int_bugzilla.php文件中添加了 checkBugID_existence() 方法后,就可以从TestLink中直接把BugZilla里面的BUG ID和某个测试关联起来了。

年终总结,新年展望

2009年,毕业后的第二年,上半年在MySpace,下半年在FreeWheel。测试技术上,有1年多的白盒测试经验,不过由于工作调动,现在已经没有做了,不过还好,还没有离开代码。在新的单位里面主要跟以下东西打交道,数据仓库,ETL工具,报表脚本,自动化回归测试框架,在线广告的业务逻辑。

在FreeWheel已经4个月了,工作开始上手,新的公司同事的素质都非常高,就我们Core来说吧,清华北大的占了一大半,第一次跟那么多非常聪明的人一起工作,有点兴奋,压力。公司发展的势头不错,我从进公司到现在,短短四个月,ad servering的流量应该翻了5倍吧。明年应该会增长的更加快,cool!

明年工作上主要还是集中在数据库相关技术,自动化回归测试框架,Python应该是主要的编程语言。

keep learning…

自动化测试中的sleep

最近在修改公司现有的一个自动化测试框架,里面用了很多time.sleep()方法,看着不是很爽,为什么我觉得sleep方法在自动化测试中不应该过多的使用呢,我甚至觉得应该尽可能避免sleep方法的使用,sleep可以作为增加自动化测试稳定性的手段,但是不能依赖sleep来让自动化系统稳定。

举个例子,如果一个UI的自动化测试,需要等待某个页面load完成以后才进行操作,那么需要对那个页面是否已经Load完成进行判断,而不应该sleep(x),x是一个magic number,有时候1、2秒就足以,有时候它却不知道有多大,因为已经超时了!那如果我们在check页面状态之前做一个短时间的sleep,那么在某些场合下可以增加这个自动化测试的稳定性,但是最终整个自动化测试的脚本是不会依赖于这个sleep的语句来达到稳定的。

在做自动化测试的时候,最常见的两种判断就是1. 某程序已经成功启动,某页面已经加载完毕。 2. 某程序已经正常关闭,某服务已经顺利停止。

回到实际的工作,我要判断被测的程序是否已经正常启动,可以用系统提供的一些工具,或者调用一些接口,例如SNMP命令,或者是调用一下Lua脚本等,如果他们都返回我们期望的数据,那么可以认为程序已经成功启动了。反之,如果前面的这些命令出错了,那么我也可以认为程序已经是关闭了的。

要实现自动化测试,就必须要让测试代码每时每刻都掌握着被测系统的状态,sleep方法会让自动化测试脚本的行为变得诡异

谁来保证测试工作的质量

在一个公司或者是某个组织里面,测试人员扮演的角色通常都会被认为是软件质量的保证者,把关者,仿佛经过测试的产品都是没有任何缺陷的。但事实大家都知道,即使经过多么“完美”测试的产品,总免不了在发布以后还会发现或多或少的问题。

以前在MySpace做测试的时候,主要是写代码来测试一些接口、模块等。这样就会出现一个问题,我用一段程序A去测试层序B,那么测试代码也是程序,如何保证程序A的正确性呢?OK,我们可以写一个程序C去测试程序A,由此来保障程序A的正确性,但是程序C的正确性又由谁来保证呢?who watches the watchers?当时只有我一个人负责白盒测试,最多也就是让开发帮忙看看。其实对于一段程序,只要写的足够简单,那么就可以认为这段简单的程序的正确性是能得到保证的。所以我一直都给自己强调,单元测试的代码不要写的复杂,尽可能不用判断,让测试代码顺序执行下去。

但是对于功能测试来说,怎么样才能尽可能地保证测试的方法,测试的数据,测试的覆盖率是能达到某项标准的呢?在这次MRM 2.9 Release的测试过程中,我们引入了peer review的做法,一个功能点一般会以ticket的形式存在,每个人拿到ticket的时候首先自己设计测试用例,包括测试数据的准备,用什么样的方法等等。然后找另外一个同事来review自己的用例。这样做的好处有:

  1. 强迫自己有一个较为系统的测试用例设计,因为这个是需要给同事看,并且让别人看懂的
  2. 同事之间的knowledge share在不知不觉中就达到
  3. 两个人的review总是比一大群人坐到会议室里面要有效,帮助提高测试覆盖率,尽可能避免测试盲点
  4. 互相监督

Python多行注释技巧

Python语言本身是没有注释多行的支持的,如果需要注册多行,可以用一个取巧的方法,就是把需要注释的代码块用三个括号括起来,赋值为一个永远都不会使用的字符串变量,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
__devilcomment = '''
if bCmpLog == "True":
        self.appendAdsLogToCmpLog("")
 
if bCmpBinaryLog == "True":
        self.appendBinaryAdsLogToCmpLog(res)
 
if bCompareResp == "True":
    self.appendResponseToCmpLog(response_strs)
 
print "move new and debug logs"
self.tools.move (src_db,dst_db)
'''

用PDB库调试Python程序

如果使用过微软技术的朋友应该体会过微软的Visual Studio系列IDE给debug程序带来的方便,换了个工作就没有Visual Studio了,对于我这种从未在非GUI环境下调试过程序的人来说实在有点不爽,今天花了点时间看了一下Python自带的pdb库,发现用pdb来调试程序还是很方便的,当然了,什么远程调试,多线程之类,pdb是搞不定的。

用pdb调试有多种方式可选:

1. 命令行启动目标程序,加上-m参数,这样调用myscript.py的话断点就是程序的执行第一行之前
python -m pdb myscript.py

2. 在Python交互环境中启用调试
>>> import pdb
>>> import mymodule
>>> pdb.run(‘mymodule.test()’)

3. 比较常用的,就是在程序中间插入一段程序,相对于在一般IDE里面打上断点然后启动debug,不过这种方式是hardcode的

if __name__ == "__main__":
    a = 1
    import pdb
    pdb.set_trace()
    b = 2
    c = a + b
    print (c)

然后正常运行脚本,到了pdb.set_trace()那就会定下来,就可以看到调试的提示符(Pdb)了

常用的调试命令

  • h(elp),会打印当前版本Pdb可用的命令,如果要查询某个命令,可以输入 h [command],例如:“h l” — 查看list命令
  • l(ist),可以列出当前将要运行的代码块

(Pdb) l
497 pdb.set_trace()
498 base_data = {}
499 new_data = {}
500 try:
501 execfile(base_file_name,{},base_data)
502 -> execfile(new_file_name,{},new_data)
503 except:
504 logger.writeLog(“error! load result log error!”)
505 print “load cmp logs error!”
506 raise Exception, “load cmp logs error!”
507

  • b(reak), 设置断点,例如 “b 77″,就是在当前脚本的77行打上断点,还能输入函数名作为参数,断点就打到具体的函数入口,如果只敲b,会显示现有的全部断点

(Pdb) b 504
Breakpoint 4 at /home/jchen/regression/regressionLogCMP.py:504

  • condition bpnumber [condition],设置条件断点,下面语句就是对第4个断点加上条件“a==3”

(Pdb) condition 4 a==3
(Pdb) b
Num Type Disp Enb Where
4 breakpoint keep yes at /home/jchen/regression/regressionLogCMP.py:504
stop only if a==3

  • cl(ear),如果后面带有参数,就是清除指定的断点(我在Python2.4上从来没成功过!!!);如果不带参数就是清除所有的断点

(Pdb) cl
Clear all breaks? y

  • disable/enable,禁用/激活断点

(Pdb) disable 3
(Pdb) b
Num Type Disp Enb Where
3 breakpoint keep no at /home/jchen/regression/regressionLogCMP.py:505

  • n(ext),让程序运行下一行,如果当前语句有一个函数调用,用n是不会进入被调用的函数体中的

[Read the rest of this entry...]

在Lua中实现简单的StringBuffer

在Lua中,字符串是一个常量,如果用字符串连接符“..”把2个字符串连接起来,例如first_str = first_str .. second_str,那么原来的first_str和second_str就会作为垃圾等待回收,first_str引用的是一个新的字符串,如果在程序里面有大量的字符串连接操作的话,性能会十分低下。Lua是一个很简洁的语言,他没有StringBuffer的实现,但是其实我们可以动手写一个简单的StringBuffer实现,来避免性能的问题。

首先定义一个叫StringBuffer的table,使得这个StringBuffer被调用的时候看起来像是面向对象的样子 :)
然后分别定义两个方法append和tostr,实现的原理就是:append用table来保存所有字符串,tostr把保存了字符串的table用concat转成真正的字符串。

StringBuffer = {}
StringBuffer.append =  function(t, str)
if t and str then
    table.insert(t, str)
end
end
StringBuffer.tostr =  function(t)
if t then
    return table.concat(t)
end
end
StringBuffer.new = function() return {} end

调用的时候大概如下,摘录了一段代码。。。

all_assets = StringBuffer.new()
for asset in ctx:allassets() do
    StringBuffer.append(all_assets, asset:id())
    StringBuffer.append(all_assets, ', ')
end
result = StringBuffer.tostr(all_assets)
print (result)

在Lua中实现这样的一个StringBuffer,既可以避免潜在的性能问题,又可以使得代码看起来更加易懂~好了,重构以前的代码去了。。。

敏捷测试只是手段不是目的

首先我要声明:本人不懂敏捷测试。
敏捷开发在这两年一直很火,很多时候作为一个测试人员,经常会“被敏捷”。一些常见的现象有,当你所要文档的时候,相关人员可能会告诉你“我们是敏捷开发,没有文档”;当你索要进度表的时候,答案也是相似的。既然软件开发已经迈入了敏捷时代,那么软件测试还能原地踏步麽?那什么是敏捷测试?我不知道,不过我想说说我在“敏捷”组织里面进行软件测试的经历。
由于我不懂敏捷测试,所以我不知道它的定义,我只能通过“敏捷测试不是传统测试”这样的思路来开展工作。
首先,那种拿着各种需求文档设计文档来设计Test Case的日子是一去不复返了,因为敏捷开发强调的是能工作的软件比漂亮的文档要有用,人和人之间的沟通胜于文档。所以作为我,一个测试员,我对被测软件的知识的了解来源有2个方面,第一是代码,第二是开发人员。
其次,如果在测试过程中发现一个问题,第一件事情并不是把这个问题记录在Tracking system里面,而是找相关的开发和产品进行确认,究竟这是不是一个真的Defect。
然后,需要知道一个现在很流行的新玩意儿:探索性测试(Exploratory Testing)。每个人对Exploratory Testing的定义都不一样,最近两个大牛(James Bach, James Whittaker)也在掐架。我觉得如果简化这个定义的话,就是我们一边熟悉被测软件,一边进行测试,在测试进行的过程中,随着对软件的熟悉,继而设计出新的Test case来对被测软件进行测试,我个人觉得就像是一个小的迭代一样。
最后,忘记敏捷测试这个词。我觉得现在业界能说清楚这个概念的人很少,所以没有必要在这些定义上纠结。人们为什么要实施敏捷开发,是因为想交付更好的软件;我们为什么要实施敏捷测试,是想提高测试的质量以及生产力,从而帮助公司交付更加高质量的产品,至于是不是真正的敏捷,又有什么重要的呢