记得以前面试过一个女孩,她认为软件测试就是点击网页,囧,作为一名软件测试工程师,我当时真是无地自容啊。相信很多人都把这个职业想象的非常简单,作为软件测试工程师的我,是有必要普及一下软件测试的童鞋都需要在哪些方面提高自己的。
1.分析能力。软件测试的核心其实应该就是设计测试用例了(具体啥样的用例设计,请参见《什么样的测试用例是好的》),而设计测试用例,就是依赖与分析能力了。这里我们不说那些常用的设计方法,从一个稍高的层面上来讲,可以说就是怎么将一个复杂的系统进行抽象,分析拆成几个不同的维度,结合维度可能出现的情况进行有选择的组合,以最小成本获取最大的收益。无法将一个复杂系统拆解成简单的维度,是没法做好用例设计的
2.编程语言。语言其实就像说话一样,只不过我们常说的英语日语之类是与人沟通,计算机语言就是与计算机进行沟通的。对于测试工程师来说,精通一门语言,熟悉其它几门语言是有必要的。对于不同语言编写的被测程序,是有不同特点的,如果对实现的语言不了解,无法进行白盒测试,没法看代码diff(结合代码diff做测试)来提高效率。对于特点不了解,可能也会导致自己漏掉部分内容。
3.设计能力。不要认为设计能力就是开发工程师的事情,拥有好的设计能力,就可以在设计评审的时候多提意见,促进开发工程师使用好的设计,不仅对开发有好处,对测试也是很有好处的。这样才能防患于未然,不仅自己的劳动力,也节省团队的劳动力。
4.对业务的理解。对业务的理解越充分,就越能够理解最终用户的需求,促进产品设计使用好的方式,促进产品成功。难道你想做一大堆不成功的项目么,那样是多么没有成就感的一件事啊。
5.自动化相关的考虑。随着项目越来越多,系统的测试项目也会积累的越来越多,每次有新功能了,难道要用手工来回归一下原有的case么。自动化测试是提高回归测试效率的唯一解决方案(如果你说还有解决方案就是不回归,我…),以高效率促进高质量,才是一个良性循环的发展方式啊。
嗯,以高效率促进高质量,我觉得很有很有道理。
代码diff与测试相结合,可以对测试起到较明显的促进作用,通常都是用在模块的功能升级使用。结合代码diff做测试,好处主要有下面几点:
1.提高旧有case回归的效率。从代码diff里面,可以看到旧有功能哪些地方没有任何改动,这样就可以增加信心,节省对旧有功能回归测试的工作量。当然,如果升级导致旧有功能的前置条件产生变化,还是需要注意的
2.明确case的重点。知道变动的内容,就可以根据修改的代码设置case,有针对性的测试。
3.辅助定位发现的问题。
4.直接发现bug或者代码设计上的问题。
5.测试人员的经验积累。经常看别人的代码diff,积累经验,就能加深自己对项目的理解,判断大致什么地方容易出现问题,提高职业素养。
diff少的时候,就可以直接发现问题了,这个非常有用,可以极大的提高效率。如果对被测对象的了解比较深入,通过查看diff即可知道新功能修改的是否到位,也可看出新增加的地方是否处理很山寨,不具备通用性等。另外比如实现细节上,看看申请的内存是否会溢出等也可以在这里处理。
有的时候,代码diff很多,看diff可以只关心重点内容:
1.if语句。新增或者修改的if标志着新旧功能点所走的不同逻辑分支。通过查看修改点的判断参数来设计case覆盖不同的分支。另外也要关注是否条件出现问题,程序走到错误分支的情况。
2.新增的函数。这种情况下,新增的函数通常都是为了满足新功能增加的部分逻辑,这时就设计case重点照顾一下新增的函数。
3.循环。比如修改了循环的条件,里面增加了break等也是属于流程分支改变的情况。
结合代码diff的作用很明显,但貌似总结这个的文章也很少,而实际用到的也是比较细小,零碎的东东,在这里先暂时列出一些,开启话题,逐渐补充吧。
作为测试人员,设计测试用例是干活的第二步(第一步当然是了解测试对象嘿嘿),这一步做的好与否,对后续工作起着决定作用,那么如何评价一个测试用例的好坏,或者说,设计的成功与否呢,本文大概讨论一下,抛砖引玉吧,记录在这里,看看是不是可以作为一次团队讨论的话题。
在此之前,我们需要明确测试工程师的工作原则:用最小的成本找出最多的问题。
1.用例覆盖程度
毫无疑问,这一点应该是最重要的,无需多说,覆盖率最大化是一套测试用例的最重要评价标准,如果漏测就杯具了。
2.用例是否已经达到工作量最小化
在满足用例覆盖程度最大化的前提下,应该尽量减小执行用例所需要的工作量。这些方面的方法有不少,如条件覆盖,分支覆盖,正交覆盖等方法。面对不同的测试对象,也有不同的方法来保证:对于网页背后的php逻辑,可以通过在网页上测试后,用一些工具比如xdebug来统计代码覆盖率;对于向外提供接口的server,采用的方式就是分析在外面暴露的接口设计用例,大致的通过接口参数来估计一下分支判断的情况。
3.用例的分类以及描述是否足够清晰
用例的分类,在这里是指相同类型的用例是否放在一起了。例如:接口类的用例,参数的取值范围是1-3,但是现在却传入4;数据类用例,状态机现在位于状态2,却要求状态跳转到无法到达的4;逻辑类用例,正常功能的产出等。将相同类型的用例放在一起,有助于理清思路,清楚了解用例设计是否完备。
用例的描述,是指描述的清晰程度是否能够形成文档。例如上面参数取值范围的例子,用例这样写:“传入错误的值”或者“传入非1-3的值”,明显没有写成“传入值4”有效。这与写程序一样,总是写闭区间的范围而不是开区间。
4.用例是否表明了测试目的
写明用例的测试目的,对文档的易于理解性和工作交接的好处不言而喻,现代软件工程不可能只有一个人在做事情,项目于人员的变动也是难免的。在过程中留下足够的信息,可以在后续工作提高很多效率。
5.测试用例的易于维护性
如果被测对象有所升级,测试用例的说明或者脚本是不是容易维护呢?例如在有状态机的情况下,测试用例之间是相互依赖的(即需要一定的执行顺序),这样被依赖的用例修改后,后端不需要同步根据修改。而如果用例之间没有相互依赖关系(如用例是自己造的数据,不是依赖于前端的产出),可能一旦有变化,就需要修改这两个。当然,这两种情况不能绝对的说哪种好,是需要看实际使用时候的情况进行取舍的。不过,通过一些系统性的工具支持,也会出现一种做法绝对性的好于另外一种的情况,情况很多,做法也有很多,在这里就不多说了。
说了这么多,其实这个第二步,还是严重依赖于第一步的,如果对测试对象的需求,实现等都不了解,设计用例也就无从谈起了。
作为一个QA,我本来是很少写代码的,不过这段代码用的次数比较多,每次用的时候都改一些,比较烦,所以整理了一个通用的,作为个人代码库的第一块石头吧
升级于2010-7-26,修复bug
class sql_runner{
private static $_arr_self = array();
private $_addr = false;
private $_user = false;
private $_passwd = false;
private $_db_connection = false;
static function get_instance($addr, $user, $passwd){
$key = $addr.'#'.$user.'#'.$passwd;
if(false === array_key_exists($key, self::$_arr_self)){
sql_runner::$_arr_self[$key] = new sql_runner($addr, $user, $passwd);
}
return sql_runner::$_arr_self[$key];
}
private function __construct($addr, $user, $passwd){
$this->_addr = $addr;
$this->_user = $user;
$this->_passwd = $passwd;
}
public function run_sql($sql){
if(false === $this->_db_connection || !mysql_ping($this->_db_connection)){
UB_LOG_DEBUG("connecting db $this->_addr, $this->_user");
$this->_db_connection = mysql_connect($this->_addr, $this->_user, $this->_passwd);
if(false === $this->_db_connection){
UB_LOG_FATAL("connect db failed: ".mysql_error());
return false;
}else{
mysql_set_charset('latin1', $this->_db_connection);
}
}
$result = false;
$result = mysql_query($sql, $this->_db_connection);
if(false === $result){
UB_LOG_FATAL("[$sql] execute failed:". mysql_error($this->_db_connection)."\n");
return false;
}
$result_arr = array();
if(is_resource($result)){
while($row = mysql_fetch_assoc($result)){
$result_arr[] = $row;
}
mysql_free_result($result);
}
UB_LOG_DEBUG("[$sql] execute succed, selected result:".print_r($result_arr, true));
return $result_arr;
}
}
/**for log functions*/
if(!function_exists('UB_LOG_DEBUG')){
function UB_LOG_DEBUG($log){
print($log);
}
}
if(!function_exists('UB_LOG_FATAL')){
function UB_LOG_FATAL($log){
print($log);
}
}
我看网上应该有不少搜索这个区别的问题,但是回答的都不全面,其中sigterm与sigint尤其有一点区别比较重要,但大都没有提及,今天我就遇到了这个问题,纠结了20分钟才搞明白咋回事。
首先,对于说这几个信号都是终止程序运行的说法不太准确,因为程序收到信号后,如果不对信号处理,就会导致程序退出,但如果程序捕获信号进行处理,按照它的逻辑,它是不一定会退出的。
在这三个信号中,sigkill是不能被捕获的,程序收到这个信号后,一定会退出。这就是kill -9一定能保证将程序杀死的原因。
下面说一下sigterm与sigint的区别,其中有一点区别区别很多文章都没有提及,也是我写这篇blog的原因(如果人家都写了,我就不用写了呗)
| 信号 |
产生方式 |
对进程的影响 |
| sigint |
通过ctrl+c将会对当进程发送此信号 |
信号被当前进程树接收到,也就是说,不仅当前进程会收到信号,它的子进程也会收到 |
| sigterm |
kill命令不加参数就是发送这个信号 |
只有当前进程收到信号,子进程不会收到。如果当前进程被kill了,那么它的子进程的父进程将会是init,也就是pid为1的进程 |
下面这两个代码片段就能够验证这种情况(注意使用pcntl的时候,一定要declare ticks,要不然会杯具的发现函数没有被调用,进程不退出,信号发过去没有作用。php手册竟然没有强调这一点):
文件:loadhelper.php
#为了pcntl能够截获信号
declare(ticks = 1);
$arr_processes = array();
function terminate($signo){
echo "aaaaaaaaaaa\n";
}
pcntl_signal(SIGTERM, "terminate", true);
pcntl_signal(SIGINT, "terminate", true);
foreach($argv as $key => $operation){
if(0 === $key){
continue;
}
$pipes = array();
$process = proc_open($operation, array(), &$pipes);
if(false === $process){
exit(-1);
}
$arr_processes[] = $process;
}
while(true){
sleep(100);
}
文件:child.php
declare(ticks=1);
pcntl_signal(SIGINT, "terminate");
pcntl_signal(SIGTERM, "terminate");
function terminate($signo){
echo "test_child\n";
}
while(true){
sleep(100);
}
使用命令php loadhelper.php “php test.php”可以启动这个测试。
1.输入ctrl+c发送sigint可以看到,父进程与子进程的terminate都得到了执行,都有输出,但父进程不会退出,因为子进程还没有退出
2.通过kill向父进程的pid发送sigterm,可以看到,只有父进程输出
遗留问题:
父进程(loadhelper)接受到一次信号后,如果在terminate函数中调用exit,它还是不能退出的,因为还有子进程没有退出。但是从此以后它就不能再接收信号了(子进程还是能够接收到sigint),可能是exit使进程进入了待回收状态,具体还 需要后续在分析一把。
在这里,我不得不再一次感叹php语言库函数的山寨与不专业。getopt函数就是一个典型的例子,通常用的时候,大家可能觉得没有什么,但在某些情况 下,就真的让人很囧。一个简单的函数,稍微多花几分钟就弄得更好一些了,但这个语言有个随意的开端,就有个随意的实现啊。
在linux中,使用getopt时候,有两种情况:
1.取得的参数解析成字符串:“php test_arg.php -c abc”,这种情况c参数取得的结果就是abc这个字符串
2.取得的参数解析成数组:“php test_arg.php -c abc -c abc123”,这种情况c参数取得的结果就是包含abc与abc123的数组
但是遇到这种情况呢:“php test_arg.php -c abc*”?由于linux的shell已经帮程序做了输入参数的解析,这时候c参数得到的既不是abc*这个结果也不是一个数组,而是被shell展开成了很多文件名后的第一个。
可能getopt用的童鞋很少,但这种山寨的设计,实在太让人憋屈了,自己花个10分钟写一个就比它的要好,为了避免大家重复劳动,分享一个代码片段
function mygetopt(){
global $argv;
$result = array();
$current_key = false;
foreach($argv as $opt){
$matches = array();
if(1 === preg_match("/^-{1,2}(.*)$/", $opt, $matches)){
$current_key = $matches[1];
if(false === isset($result[$current_key])){
$result[$current_key] = false;
}
}else if (false !== $current_key){
if(false === $result[$current_key]){
$result[$current_key] = $opt;
}else{
if(false === is_array($result[$current_key])){
$result[$current_key] = array($result[$current_key]);
}
$result[$current_key][] = $opt;
}
}
}
return $result;
}
为了方便使用,将新版本的getopt函数设置为不接受任何参数,但是解析的结果可以输出所有的参数内容。因为php官方的getopt函数使用后,也无非是对输出的数组进行foreach之后进行switch,还不如方便点,直接解析所有呢。除了这一点,这个getopt函数的输出结果与php官方的完全一致
php官方getopt函数参考文档:http://cn.php.net/manual/en/function.getopt.php