理想国

我要看尽这世间繁华


  • 首页

  • 归档

  • 标签

  • 分类

  • 关于

  • 搜索

黑盒测试用例设计技术--等价类划分法

发表于 2015-03-18 | 更新于 2019-04-03 | 分类于 Testing , 功能测试

本文通过案例的形式,详细讲解黑盒测试用例设计技术中的等价类划分法。

等价类划分是一种典型的黑盒测试方法,其原理是把程序的输入域划分成若干部分(子集),然后从每一个子集中选取少数具有代表性的数据作为测试用例。

通过等价类划分,可以在尽可能覆盖所有测试路径的前提下,大幅度减少测试用例的数目。

本文的主要内容有:

  • 等价类的概念介绍
  • 划分等价类的原则
  • 根据等价类设计测试用例的方法
  • 案例演示

划分等价类

等价类是指某个输入域的子集合。在该子集合中,各个输入数据对于揭露程序中的错误都是等效的。并合理的假设,测试某等价类的代表值就等于对这一类其它值的测试。

等价类划分有两种不同的情况:

  • 有效等价类:指对于程序的规格说明来说是合理的、有意义的输入数据构成的集合。
  • 无效等价类:指对程序的规格说明是不合理的或无意义的输入数据所构成的集合。对于具体的问题,无效等价类至少应有一个,也可能有多个。

在设计测试用例时,要同时考虑有效等价类和无效等价类,以此验证软件在正常操作和异常操作时是否都能正常运行。

确定等价类的6条原则:

1、在输入条件规定了取值范围或取值的个数的情况下,可以确立一个有效等价类和两个无效等价类。

  • 例1:输入值是学生成绩,输入形式为文本框,要求的输入范围是0~100
    • 有效等价类:0<=输入成绩<=100;
    • 无效等价类1:输入成绩<0;
    • 无效等价类2:输入成绩>100

2、在输入条件规定了输入值的集合或者规定了“必须如何”的条件的情况下,可以确立一个有效等价类和一个无效等价类。

  • 例2:输入值是人员性别,输入形式为文本框,要求输入的内容必须在集合{男,女}中
    • 有效等价类:性别=’男’ 或者 ‘女’
    • 无效等价类:性别=’人妖’

3、在输入条件是一个布尔量的情况下,可以确立一个有效等价类和一个无效等价类。

  • 例3:输入值是状态标识位“是否完成”,输入形式为单选下拉框,选择范围为{是,否}
    • 有效等价类:选项=’是’ 或者 ‘否’
    • 无效等价类:未进行选择操作

4、在规定了输入数据的一组值(假设N个),并且程序要对每一个输入值进行处理的情况下,可以确立N个有效等价类和一个无效等价类。

  • 例4:输入值是人员性别,输入形式为文本框,要求输入的内容必须在集合{男,女}中;不同的性别选择将跳转至不同的处理页面
    • 有效等价类1:性别=’男’
    • 有效等价类2:性别=’女’
    • 无效等价类:性别=’人妖’

5、在规定了输入数据必须遵守的规则的情况下,可以确立一个有效等价类(符合条件)和若干无效等价类(从各个角度违反规则)。

  • 例5:输入值是人员性别,输入形式为单选下拉框,要求输入的内容必须在集合{男,女}中;
    • 有效等价类:性别=’男’ 或者 ‘女’
    • 无效等价类1:未选择人员性别
    • 无效等价类2:在浏览器开发工具中将人员性别的属性值更改为’人妖’

6、在确知已划分的等价类中各元素在程序处理中的方式不同的情况下,则应再将该等价类划分为更小的等价类。

  • 例6:在例2(输入值是人员性别,输入形式为文本框,要求输入的内容必须在集合{男,女}中)的基础上,不同的性别选择将跳转至不同的处理页面
    • 有效等价类:性别=’男’ 或者 ‘女’
      • 有效等价类细分1:性别=’男’
      • 有效等价类细分2:性别=’女’
    • 无效等价类:性别=’人妖’

列出等价类表

输入条件 有效等价类 无效等价类
… … …

确定测试用例

根据已列出的等价类表,按照如下步骤确定测试用例:
1)为每个等价类规定一个唯一的编号
2)设计一个新的测试用例,使其尽可能多地覆盖尚未覆盖的有效等价类。重复这一步,最后使得所有有效等价类均被测试用例所覆盖。
3)设计一个新的测试用例,使其只覆盖一个无效等价类。重复这一步,使所有无效等价类均被覆盖。

Example1

某程序具有如下功能:输入3个正数a、b、c,分别作为三边的边长构成三角形,输出这3个数所构成的三角形类型。
用等价类划分方法为该程序进行测试用例设计。

划分等价类

分析思路:
步骤一、要求输入3个数,且3个数都为正数;参照规则5,划分为一个有效等价类和三个无效等价类。

  • 有效等价类(1):a>0; b>0; c>0;
  • 无效等价类(2):a<=0
  • 无效等价类(3):b<=0
  • 无效等价类(4):c<=0

步骤二、在有效等价类(1)的基础上,参照规则6,对该等价类进行细分;考察3个数能否构成三角形,参照规则5,划分为一个有效等价类和三个无效等价类。

  • 有效等价类(5):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a
  • 无效等价类(6):a>0; b>0; c>0; a+b<=c
  • 无效等价类(7):a>0; b>0; c>0; b+c<=a
  • 无效等价类(8):a>0; b>0; c>0; a+c<=b

步骤三、在有效等价类(5)的基础上,参照规则6,对该等价类进行细分;考察3个数能否构成等边三角形,参照规则2,划分为一个有效等价类和一个无效等价类。

  • 有效等价类(9):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; a=b=c
  • 无效等价类(10):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; a!=b 或 b!=c 或 c!=a

步骤四、在无效等价类(10)的基础上,参照规则6,对该等价类进行细分;考察3个数能否构成等腰三角形,参照规则4,划分为三个有效等价类和一个无效等价类。

  • 有效等价类(11):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; a=b!=c
  • 有效等价类(12):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; b=c!=a
  • 有效等价类(13):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; c=a!=b
  • 无效等价类(14):a>0; b>0; c>0; a+b>c; a+c>b; b+c>a; a!=b; a!=c; b!=c

设计测试用例

序号 [a,b,c] 覆盖等价类 预期输出结果
– – 覆盖有效等价类 –
1 [6,6,6] (1)(5)(9) 等边三角形
2 [3,3,5] (1)(5)(10)(11) 等腰三角形
3 [3,4,4] (1)(5)(10)(12) 等腰三角形
4 [4,5,4] (1)(5)(10)(13) 等腰三角形
– – 覆盖无效等价类 –
5 [3,4,5] (1)(5)(14) 一般三角形
6 [-1,3,2] (2) 不能构成三角形
7 [3,-1,2] (3) 不能构成三角形
8 [3,2,-1] (4) 不能构成三角形
9 [1,2,3] (1)(6) 不能构成三角形
10 [3,1,2] (1)(7) 不能构成三角形
11 [1,3,2] (1)(8) 不能构成三角形

Example2


某程序具有如下功能:文本框要求输入日期信息,日期限定在1990年1月~2049年12月,并规定日期由6位数字字符组成,前4位表示年,后2位表示月;程序需对输入的日期有效性进行校验。
用等价类划分方法为该程序的“日期检查功能”设计测试用例。

划分等价类

步骤一、要求输入6个数字字符yyyynn;参照规则5,划分为一个有效等价类和三个无效等价类。

  • 有效等价类(1):输入6个数字字符
  • 无效等价类(2):输入6个字符,存在非数字的情况
  • 无效等价类(3):输入少于6个字符
  • 无效等价类(4):输入多于6个字符

步骤二、在有效等价类(1)的基础上,参照规则6,对该等价类进行细分;考察6个数是否满足日期格式要求,1990<=yyyy<=2049,01<=nn<=12,参照规则,划分为一个有效等价类和四个无效等价类。

  • 有效等价类(5):日期格式满足要求,1990<=yyyy<=2049,01<=nn<=12
  • 无效等价类(6):yyyy不满足要求,yyyy<1990
  • 无效等价类(7):yyyy不满足要求,yyyy>2049
  • 无效等价类(8):nn不满足要求,nn<01
  • 无效等价类(9):nn不满足要求,nn>12

设计测试用例

序号 yyyynn 覆盖等价类 预期输出结果
– – 覆盖有效等价类 –
1 199307 (1)(5) 日期格式有效
– – 覆盖无效等价类 –
2 19June (2) 日期格式无效
3 19Jun (3) 日期格式无效
4 19June2 (4) 日期格式无效
5 198805 (6) 日期格式无效
6 205005 (7) 日期格式无效
7 198800 (8) 日期格式无效
8 199513 (9) 日期格式无效

基 HTTP 的 WebService 测试方法

发表于 2015-02-13 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

在《基于WSDL或SOAP的WebService测试方法–对原理的思考》一文中写道:

从通讯协议层面上来看,SOAP报文只是对传输的内容进行了格式封装,具体传输实现还是依赖于其它应用层协议(如HTTP)。因此我们在测试WebService时,完全可以抛开WSDL和SOAP,直接从HTTP协议层面获取请求和响应的内容,然后采用测试工具构造HTTP请求,实现对WebService的调用。

在本篇文章中,52test.org仍将在测试案例天气预报WebService服务的基础上,详细介绍如何通过HTTP协议测试WebService。

获取HTTP请求

在天气预报WebService服务的各个接口介绍页面中,均包含一个测试工具,可对接口进行调用测试。

例如,接口getWeatherbyCityName的测试工具如下图所示。在theCityName参数框内输入城市的名称,点击【调用】按钮,即可实现对getWeatherbyCityName接口的调用,并获得返回结果。

在WebService的调用过程中,我们无需关注它具体是采用什么样的通讯协议,因为不管是何种通讯协议,具体传输实现还是会依赖于HTTP。因此,我们可以通过HTTP抓包工具对WebService调用过程中的通讯交互数据包进行捕捉。

在这里我们采用Fiddler Web Debugger进行演示。在浏览器中调用接口getWeatherbyCityName的测试工具时,在Fiddler中抓取到对应的HTTP请求,如下图所示。

从该HTTP请求可以获得如下关键信息:

  • HTTP请求类型为POST,HTTP版本为1.1
  • 请求的URL为:http://webservice.webxml.com.cn/WebServices/WeatherWebService.asmx/getWeatherbyCityName
  • POST的数据包为:theCityName=%E5%B9%BF%E5%B7%9E;POST数据包中对中文进行了URL转码

使用获取到的HTTP信息,可构造HTTP请求,从HTTP协议层面对WebService进行调用。

在Fiddler中构造HTTP请求

在Fiddler中,可使用Composer对HTTP请求进行构造,如下图所示。

在Request Body中,修改请求参数theCityName为不同的城市(例如,重庆),Execute请求,查看返回结果。

在LoadRunner中构造HTTP请求

若要进行性能测试,则需在性能测试工具中构造HTTP请求,再通过多进程或多线程机制实现并发压力测试。

在LoadRunner中,可在Web(HTTP/HTML)虚拟用户协议中,采用web_custom_request函数来构造HTTP请求。

具体的代码实现及回放结果如下图所示。

虽然在LoadRunner中返回的中文显示为乱码,但是从城市编码(57516)可以看出,脚本执行后返回了正确的结果。

写在后面

通过这篇文章可以看出,在通讯协议层面上对应用服务进行测试时,可以采用更底层的协议。当然,由于HTTP协议是基于Socket的,在测试WebService时也可以从Socket协议层面进行测试,但估计没人会那么去做。

至此,我们已经对主流的WebService测试方法完成了总结,相关的文章如下:

  • 测试工程师的自我修养–理解WebService
  • LoadRunner基于WSDL的WebService测试方法
  • LoadRunner基于SOAP的WebService测试方法
  • 基于WSDL或SOAP的WebService测试方法–对原理的思考
  • 基于HTTP的WebService测试方法

基于WSDL或SOAP的WebService测试方法--对原理的思考

发表于 2015-02-12 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

截止至今,我们已经总结了两种WebService测试方法,分别对应了两种应用场景:

  • 在只获知WSDL的情况下,如何采用LoadRunner测试WebService
  • 在具有SOAP报文的情况下,如何采用LoadRunner测试WebService

在文章《LoadRunner基于WSDL的WebService测试方法》和《LoadRunner基于SOAP的WebService测试方法》中,已经详细地描述了两种测试方法的具体实现方式,即使对于一个新接触WebService的测试人员,按照文章中的方法也基本都能完成测试任务。

但是,读到这里你是否会感到困惑:这两种测试方法之间的主要区别在哪儿?两者之间是否存在什么内在联系呢?

没错,虽然从LoadRunner的操作方式(导入WSDL和导入SOAP)与最终生成的封装函数(web_service_call和soap_request)来看,两者完全是两种不同的测试方法,但是从原理层面上讲,他们本质上都是一样的。

别惊讶,我们先来回顾一下WebService的概念。可参考《测试工程师的自我修养–理解WebService》。

在WebService中,涉及到的技术名词主要有包括WSDL和SOAP。其中,WSDL就如同WebService的规格说明书,它详细描述了WebService提供的服务接口,包括每个接口的方法名称、接受的参数类型,以及返回的数据结构。除此之外,WSDL还描述了网络传输协议,即WebService支持以什么协议进行通讯交互。而SOAP作为一种XML编码格式的通讯协议,在WebService中应用得最为广泛,基本上绝大多数WebService都支持SOAP协议,以至于谈到WebService时,会不由自主地联想到SOAP,甚至非常多的人混淆了WSDL和SOAP的概念,常在网上提问两者的区别。

事实上,对于WebService而言,SOAP并不是必须的,我们完全可以采用别的通讯协议来代替它。

现在再回到前面的问题。

在基于WSDL的WebService测试方法中:LoadRunner导入WSDL后,对WSDL进行解析,分析出里面包含的接口以及支持的通讯协议,并以友好的交互界面展示给测试人员。测试人员在界面中选择接口(Operation)和通讯协议(Port Name)后,LoadRunner进行封装,生成了web_service_call函数。

而在基于SOAP的WebService测试方法中:给定的SOAP报文已经限定了特定的接口和输入参数,当然,既然是SOAP报文,采用的通讯协议当然就是SOAP协议,并且在报文中指明了特定的SOAP版本。LoadRunner导入SOAP报文后,经过封装,生成了soap_request函数。

说到这里大家应该比较清楚了,不管是基于WSDL还是基于SOAP,最终无非都是指定WebService接口函数和通讯协议,然后,输入参数,获取响应结果。至于LoadRunner采用了什么样的封装函数,那也只是不同的表现形式而已。

其实,从通讯协议层面上来看,SOAP报文只是对传输的内容进行了格式封装,具体传输实现还是依赖于其它应用层协议(如HTTP)。因此我们在测试WebService时,完全可以抛开WSDL和SOAP,直接从HTTP协议层面获取请求和响应的内容,然后采用测试工具构造HTTP请求,实现对WebService的调用。

在下一篇文章中,52test.org将详细介绍如何通过HTTP协议测试WebService,敬请关注。

使用SIPp对基于SIP协议的IP电话通信服务器进行性能测试

发表于 2014-05-11 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

背景知识介绍

IP电话

IP电话是指在IP网络上打电话。所谓“IP网络”就是“使用IP协议的分组交换网”的简称。
常见的IP电话有VoIP(Voice over IP),Internet Telephony 和 VON(Voice over Net)。

IP电话网关

IP电话网关(IP Telepathy Gateway),是公用电话网(即公用电路交换电话网,又称传统电话网或电信网)与IP网络的接口设备,其作用包含两个方面:

  • 在电话呼叫阶段和呼叫释放阶段进行电话信令的转换
  • 在通话期间进行话音编码的转换

IP电话网关的出现,实现了PC机用户之间的IP电话通话(无需经过IP电话网关),PC机用户与固定电话用户的IP电话通话(仅需经过IP电话网关1次),以及固定电话用户之间的IP电话通话(需要经过IP电话网关2次)。

IP电话所需要的几种应用协议

在IP电话的通信中,至少需要两种应用协议,即信令协议和传送协议。
其中,信令协议的作用是帮助主叫者在因特网上找到被叫用户。在公用电话网中,电话交换机根据用户所拨打的号码就能够通过合适的路由找到被叫用户,并在主叫和被叫之间建立一条电路连接。这些都是依靠电话信令(Signaling)实现的。我们听到的振铃音、忙音或一些录音提示,以及打完电话挂机释放连接,都是由电话信令来处理的。现在电话网使用的信令是7号信令SS7。利用IP网络打电话同样也需要IP网络能够识别某种信令。但由于IP电话往往要经过已有的公用电话网,因此IP电话的信令必须在功能上与原有的7号信令相兼容,这样才能使IP网络和公用电话网上的两种信令能够互相转换,从而做到互操作。现有的与信令有关的协议为H.323和SIP,本文只介绍SIP协议。
话音分组的传送协议,其作用是使用来进行电话通信的话音数据能够以时延敏感属性在因特网中传送。常见的传送协议为RTP。

SIP协议

SIP协议,即会话发起协议(Session Initiation Protocol),是使用文本方式的客户服务器协议。
SIP系统只有两种构件,即用户代理(user agent)和网络服务器(network server)。
用户代理包含两个程序,即用户代理客户端UAC(User Agent Client)和用户代理服务器UAS(User Agent Server);前者用来发起呼叫,后者用来接受呼叫。
网络服务器分为代理服务器(proxy server)和重定向服务器(redirect server)。代理服务器接受来自主叫用户的呼叫请求(实际上是来自用户代理客户端UAC的呼叫请求),并将其转发给被叫用户或下一跳代理服务器;若转发给下一跳代理服务器,则下一跳代理服务器再把呼叫请求转发给被叫用户(实际上是转发给用户代理服务器UAS)。重定向服务器不接受呼叫,它通过响应告诉客户下一跳代理服务器的地址,由客户按此地址向下一跳代理服务器重新发送呼叫请求。
SIP的会话共有三个阶段:建立会话、通信和终止会话。图1给出了一个简单的SIP会话的例子。其中建立会话阶段和终止会话阶段都是使用SIP协议,而中间的通信阶段是使用如RTP这样的传送实时话音分组的协议。

{: .center}
SIP_Simple_Session
图1 SIP的简单会话过程

在图1中,主叫方(Tesla)先向被叫方(Marconi)发出INVITE报文,这个报文中含有双方的地址信息以及一些其它信息(如通话时话音编码方式等);被叫方收到请求后,会返回180 Ringing的状态码,并处于振铃状态;若被叫方接受呼叫请求,则返回200 OK的状态码;主叫方再发送ACK报文作为确认(类似于TCP建立连接时的三次握手),然后双方便完成连接,可以进行通话了;通话过程中,双方中的任何一方都可以发送BYE报文以终止会话。如上便是SIP会话的全过程。

SIPp工具介绍

SIPp软件简介

如下是SIPp官方网站上关于SIPp的介绍。

SIPp is a free Open Source test tool / traffic generator for the SIP protocol. It includes a few basic SipStone user agent scenarios (UAC and UAS) and establishes and releases multiple calls with the INVITE and BYE methods. It can also reads custom XML scenario files describing from very simple to complex call flows. It features the dynamic display of statistics about running tests (call rate, round trip delay, and message statistics), periodic CSV statistics dumps, TCP and UDP over multiple sockets or multiplexed with retransmission management and dynamically adjustable call rates.
SIPp can be used to test various real SIP equipment like SIP proxies, B2BUAs, SIP media servers, SIP/x gateways, SIP PBX, … It is also very useful to emulate thousands of user agents calling your SIP system.

SIPp的安装(Windows)

SIPp几乎可以运行在所有UNIX平台上,HPUX、Tru64、Linux (RedHat, Debian, FreeBSD)和Solaris/SunOS。并且,SIPp已被移植到Windows平台。

  • 在Linux/Unix平台上,SIPp采用源代码的形式进行提供,需要通过编译源代码的方式进行安装。
  • 在Windows平台上,SIPp提供了预编译的可执行文件,可以直接进行安装;也可以通过在Windows系统中安装CYGWIN模拟Linux环境,然后通过编译源代码的方式进行安装。

通常情况下,在Windows平台中通过预编译可执行文件可以快速完成安装,操作简单且成功率高,但该种方式存在局限性,主要有如下几点:

  • Windows预编译版本的功能存在删减,不支持PCAP(语音)和密码验证功能;
  • Windows的预编译版本较为滞后,例如当前SIPp的最新版本为3.4,而Windows的最新预编译版本为3.2;
  • 相比于Linux/Unix平台,SIPp在Windows平台上运行不够稳定,特别是并发量较大时可能会出现问题,因此不适宜进行压力测试。

SIPp的工作原理

对于电话而言,每台电话机既是客户端(可拨打电话)也是服务端(可接听电话)。同理,SIPp模拟用户代理,也有两种工作模式:UAC和UAS。

SIPp中具有场景(scenario)的概念,它是一个XML格式的文件,其作用是用来描述SIP协议通讯过程。具体地,UAC和UAS分别对应一个场景文件:uac.xml用于描述客户端的呼叫,uas.xml用于描述服务端的响应。在SIPp工具中,已经内置了许多场景文件,可直接使用;测试人员也可参照场景文件的格式要求,自定义编写场景文件。

另外,SIPp采用了数据与场景分离的设计,从而使工具的使用更加灵活。其中,数据包括IP地址、端口号、电话号码、用户名等内容。通常,测试数据保存在CSV文件中,如data.csv。

SIPp是通过命令行的方式操作调用的。例如,在利用SIPp模拟UAC发起呼叫时,可在cmd窗口中运行如下命令:

1
sipp -sn uac 172.31.89.4:5060 -r 1 -rp 3000 -inf data.csv -p 7098 -i 172.31.89.242 -s 8001 -sf uac_onecall.xml –m 1000 –l 900

可以看出,如果每次都在命令行中输入命令进行运行,效率将十分低下;因此,可以将配置好的命令保存在bat批处理文件中,方便测试时直接调用。

SIPp的常用工作模式

SIPp具有三种常用工作模式:

  • 当SIPp作为UAC时,可向已有电话终端(电话机或运行在电脑上的虚拟终端)发起呼叫;
  • 当SIPp作为UAS时,可接收到现有电话终端(电话机或运行在电脑上的虚拟终端)发起的呼叫;
  • SIPp还可同时作为UAC和UAS,并使用UAC向UAS发起呼叫,以此来对代理服务器进行测试。

图2是对第三种模式的描述。

{: .center}
SIPp_Operating_Principle
图2 SIPp工作原理

在该种模式下,需先运行服务端UAS,然后再运行客户端UAC。接下来,UAC便参照uac.xml场景文件的描述,发起呼叫;而UAS则参照uas.xml场景文件的描述,对呼叫进行响应;整个通讯过程遵循SIP协议,在本文的1.4节已经进行了介绍。

SIPp的使用

SIPp的配置文件

通过本文2.3节的介绍可知,使用SIPp时会涉及到5个文件:uac.xml, uas.xml, data.csv, uac.bat, uas.bat。

其用途简要说明如下:

  • uac.xml:用于描述客户端UAC的SIP信号流程;
  • uas.xml:用于描述服务端UAS的SIP信号流程;
  • data.csv:存储数据的文件,方便在uac.xml和uas.xml中引入的相应数据;
  • uac.bat:存储SIPp命令及参数,便于执行时直接调用,模拟UAC的呼叫;
  • uas.bat:存储SIPp命令及参数,便于执行时直接调用,模拟UAS的响应。

由于uac.xml和uas.xml类似,uac.bat和uas.bat类似,本文只对uac.xml、uac.bat和data.csv三个文件进行讲解。

uac.xml

SIPp默认自带的uac.xml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">

<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[

INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Type: application/sdp
Content-Length: [len]

v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000

]]>
</send>

<recv response="100" optional="true">
</recv>

<recv response="180" optional="true">
</recv>

<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>

<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[

ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Length: 0

]]>
</send>

<!-- This delay can be customized by the -d command-line option -->
<!-- or by adding a 'milliseconds = "value"' option here. -->
<pause/>

<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[

BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Length: 0

]]>
</send>

<recv response="200" crlf="true">
</recv>

<!-- definition of the response time repartition table (unit is ms) -->
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>

<!-- definition of the call length repartition table (unit is ms) -->
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>

</scenario>

如该文件所示,UAC首先发送INVITE请求,按照预期,UAC将依次接收100、180和200状态码,然后UAC再次发送ACK进行确认,从而完成通信连接的建立;最后,UAC发送BYE请求,结束通话,从而断开通信连接。

data.csv

在data.csv文件中,第一行描述数据的读取方式,如顺序读取(SEQUENTIAL)、随机读取(RANDOM),或者自定义方式(USER)。
从第二行开始,每行描述一次呼叫时使用的数据;如果每次呼叫时调用的参数有多个,那么就可用分号(”;”)进行分离,并可在XML文件中进行调用,每行的数据从左到右依次为[field0], [field1], …。

csv文件的编写示例如下:

1
2
3
4
5
6
SEQUENTIAL
#注释将被忽略
Sarah;sipphone32
Bob;sipphone12
#This line too
Fred;sipphone94

XML调用CSV的具体方式如图3所示。

{: .center}
XML_Invoke_CSV
图3 XML调用CSV的原理

uac.bat

1
sipp -sn uac 172.31.89.4:5060 -r 1 -rp 3000 -inf data.csv -p 7098 -i 172.31.89.242 -s 8001 -sf uac_onecall.xml –m 1000 –l 900

各个参数说明:

  • 172.31.89.4:5060:远端地址和端口(在xml脚本中用[remote_ip],[remote_port]引用)
  • -r 1 -rp 3000:每三秒钟(3000毫秒)发起一个呼叫
  • -inf data.csv:引入数据配置文件
  • -p 7098:本地端口(在xml脚本中用[local_port]引用)
  • -i 172.31.89.242:本地地址(在xml脚本中用[local_ip]引用)
  • -s 8001:被叫号码(在xml脚本中用[service]引用)
  • -sf uac_onecall.xml:引用xml脚本文件,根据需要模拟的呼叫流程编写
  • -sn uac :执行默认的uac流程,如需执行自己编写的流程文件,命令中应不含此参数
  • -m 1000:发送1000次呼叫后停止并退出。
  • -l 900 :最大同时保持呼叫量,默认值为3*caps值呼叫时长,当因种种原因导致现存呼叫总数达到此值时,SIPp将停止产生新的呼叫,等待现存呼叫总数低于此值时才继续产生呼叫。
  • -trace_err跟踪所有错误消息,并把错误消息保存到文件场景文件描述的 {fileName}_{pid}_errors.log 文件中
  • -trace_screen 当程序结束时候打印统计信息并弹出屏幕(如果在后台运行的话)

使用SIPp对基于SIP协议的网络电话进行性能测试

测试内容

  • 测试某通信服务器在400组并发呼叫过程中的CPU负载峰值和内存占用率。

测试场景分析

呼叫发起端:湖北省公安厅的两台测试机,每台测试机模拟出等量的电话终端,向十堰市公安局的物理电话机发起呼叫。
电话接收端:十堰市公安局的400台物理电话机。
网络环境:湖北省公安信息网。

测试环境的网络拓扑图如图4所示。

{: .center}
Network_Topology_Map
图4 被测系统的网络拓扑图

测试方法

利用SIPp测试工具模拟电话终端,向物理电话机终端发起呼叫,保持呼叫16秒后,SIPp终止呼叫。
在测试过程中,通过命令的方式监控并记录通信服务器的CPU负载峰值和内存占用率。

测试步骤

(1)配置SIPp测试工具:在湖北省公安厅的两台测试机上分别配置SIPp的场景文件(uacTest.xml,详见附录),数据文件(CallNumber.csv,详见附录)和测试控制文件(test.bat,详见附录)。
(2)开启通信服务器的资源监控:远程登录至十堰市公安局网络电话通信服务器,执行资源监控命令,开启对通信服务器CPU负载和内存的监控。
(3)执行测试:在湖北省公安厅的两台测试机上同时执行SIPp控制文件(bat文件)。
(4)获取测试结果:测试完毕后,导出被测通信服务器的资源监控结果,提取出并发测试期间中的资源监控结果。

参考资料

  • 电子工业出版社 计算机网络(第5版)谢希仁 编著。
  • http://sipp.sourceforge.net/

附录

test.bat

1
sipp -sf uac.xml -inf CallNumber.csv -i 10.XX.XX.151 -p 5061 -s 96018702 -m 1 -r 1 10.XX.XX.114:5060 -trace_err

uacTest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">

<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[

INVITE sip:[field0]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "[service]" <sip:[service]@[remote_ip]>;tag=[call_number]
To: "[field0]" <sip:[field0]@[remote_ip]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[service]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Type: application/sdp
Content-Length: [len]

v=0
o=- 1 2 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0 8 18 101
a=rtpmap:18 G729/8000

]]>
</send>

<recv response="100" optional="true">
</recv>

<recv response="180">
</recv>

<!-- Set Ring Time -->
<pause milliseconds="8000"/>

<!-- Abort Call -->
<send retrans="500">
<![CDATA[

CANCEL sip:[field0]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "[service]" <sip:[service]@[remote_ip]>;tag=[call_number]
To: "[field0]" <sip:[field0]@[remote_ip]>
Call-ID: [call_id]
CSeq: 1 CANCEL

]]>
</send>

<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true"></recv>

<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[

ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Length: 0

]]>
</send>

<!-- This delay can be customized by the -d command-line option -->
<!-- or by adding a 'milliseconds = "value"' option here. -->
<pause milliseconds="8000"/>

<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[

BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Length: 0

]]>
</send>

<recv response="200" crlf="true">
</recv>

<!-- definition of the response time repartition table (unit is ms) -->
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>

<!-- definition of the call length repartition table (unit is ms) -->
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>

</scenario>

CallNumber.csv

1
2
3
4
5
SEQUENTIAL
96038000;
96038001;
96038002;
......

LoadRunner 中获取当前系统时间

发表于 2013-10-28 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

引言

在测试场景中,常会遇到需要提交系统时间的情况。本文对使用LoadRunner获取系统时间的几种方法进行探讨。

常用的方法有如下四种:

  • 方法一:使用LR的参数化功能
  • 方法二:使用LR函数lr_save_datetime()
  • 方法三:使用C语言标准函数库中的time()和ctime()
  • 方法四:使用C语言的tm结构,把时间分解成若干元素,再根据需求进行重组

方法一:使用LR的参数化功能

操作步骤:

1、在Parameter List窗口中新建一个参数localtime_now,Parameter type选择为【Date/Time】
2、设置Date/Time format,具体格式可参照帮助手册,例如%Y-%m-%d %H:%M:%S对应的是2013-10-28 16:43:06
3、在脚本中,利用函数lr_eval_string将参数localtime_now转换为变量tt_1
4、在脚本中引用变量tt_1

对应的脚本如下:

1
2
3
char *tt_1;
tt_1 = lr_eval_string("{localtime_now}");
lr_message("系统当前的时间为:%s", tt_1);

运行结果:
系统当前的时间为:2013-10-28 16:43:06

方法二:使用LR函数lr_save_datetime()

在LoadRunner中,函数lr_save_datetime可以将当前时间赋值给指定参数,并可在赋值时选择时间样式。

1
2
3
4
5
char *tt_2;
//获得当前系统时间,并根据设置的格式将当前时间赋值给 times
lr_save_datetime("%Y-%m-%d %H:%M:%S", DATE_NOW+TIME_NOW, "localtime_2");
tt_2 = lr_eval_string("{localtime_2}");
lr_message("系统当前的时间为:%s", tt_2);

运行结果:
系统当前的时间为:2013-10-28 17:43:16

方法三:使用C语言标准函数库中的time()和ctime()

对应的脚本如下:

1
2
3
long tt_3;
time(&tt_3);
lr_message("系统当前的时间为:%s", ctime(&tt_3));

运行结果:
系统当前的时间为:Mon Oct 28 17:43:16 2013

方法四:tm结构分解

说明:tm结构即是一个结构体,将时间分解为9个部分,将时间的各个部分赋值给不同的变量,然后根据实际需求,将各个部分进行重组后使用。

对应的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Action()
{
struct tm {
int second; //取得当前秒数(在分钟后);取值区间为[0,59]
int minute; //取得当前分钟数(在小时后);取值区间为[0,59]
int hour; //取得当前小时数(从凌晨0点开始);取值区间为[0,23]
int day; //取得当前天数(从上月结束开始);取值区间为[1,31]
int month; //取得当前月份数(从1月开始);取值区间为[0,11]
int year; //取得当前年份数(从1900年开始)
int weekday; //取得当前日期数(为了获取星期几,从上个星期日开始);取值区间为[0,6]
int yearday; //取得当前年份天数(从1月1日开始);取值区间为[0,365]
int daylight; //取得当前夏令时标识符,实行夏令时的时候,daylight取得一个正数
//不实行夏令时的进候,daylight为0;
//不了解情况时,daylight为负数
};

long timenow; //定义保存时间的变量
struct tm *now; //定义结构指针
int year, month, day, weekday, hour, minute, second;
char *week;
time(&timenow); //获取当前时间
now = (struct tm *)localtime(&timenow); //把当前时间的结构指针赋值给now
year = now->year;
month = now->month;
day = now->day;
hour = now->hour; //获取hour值
minute = now->minute; //获取minute值
second = now->second; //获取second值
weekday = now->weekday;//获取week值

switch(weekday)//判断得到中文的星期
{
case 1:week ="星期一";
break;
case 2:week ="星期二";
break;
case 3:week ="星期三";
break;
case 4:week ="星期四";
break;
case 5:week ="星期五";
break;
case 6:week ="星期六";
break;
case 0:week ="星期日";
break;
};

lr_message("使用tm结构获取的当前时间为:%d-%d-%d %s %d:%d:%d",year,month,day,week,hour,minute,second);

return 0;
}

运行结果:
使用tm结构获取的当前时间为:113-9-28 星期一 17:43:16

在Pelican中使用多说评论系统

发表于 2013-10-05 | 更新于 2019-04-03 | 分类于 效率工具

引言

Pelican默认采用的是Disqus评论系统,但由于Disqus附带的Facebook和Twitter社交功能在国内无法使用,因此我们更希望使用一些国内的SNS平台,比如QQ,新浪微博,豆瓣,人人等。在国产的评论系统中,使用较多的有多说、有言等,本文将介绍如何在Pelican博客系统中采用多说评论系统。

获取多说代码

在多说首页点击【我要安装】,进入创建站点界面;完成站点信息填写后,点击【创建】按钮,即可获得多说代码,大致如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Duoshuo Comment BEGIN -->
<div class="ds-thread"></div>
<script type="text/javascript">
var duoshuoQuery = {short_name:"leolee"};
(function() {
var ds = document.createElement('script');
ds.type = 'text/javascript';ds.async = true;
ds.src = 'http://static.duoshuo.com/embed.js';
ds.charset = 'UTF-8';
(document.getElementsByTagName('head')[0]
|| document.getElementsByTagName('body')[0]).appendChild(ds);
})();
</script>
<!-- Duoshuo Comment END -->

在上面的代码中,short_name即是注册的多说域名。

修改模板系统

获得多说代码后,在博客正文末尾对其进行引用,便可使用多说的评论功能。
由于我们采用的是模板,因此只需要在article模板中添加对多说代码的引用,便可以一劳永逸。
针对Pelican的模板系统,需要修改如下几个地方。

在 pelicanconf.py 中开启多说评论功能

模仿Pelican默认的DISQUS,在配置文件pelicanconf.py中添加如下代码。

1
DUOSHUO_SITENAME = "leolee"

需要注意的是,DUOSHUO_SITENAME需要全部字母大写,因为只有这样Pelican才会将其作为全局变量,供其它文件引用。

添加 DuoShuo_Script.html

将获取得到的多说代码保存至单个文件中,方便在其它文件中对其进行引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% if DUOSHUO_SITENAME %}
<!-- Duoshuo Comment BEGIN -->
<div class="ds-thread"></div>
<script type="text/javascript">
var duoshuoQuery = {short_name:"{{ DUOSHUO_SITENAME }}"};
(function() {
var ds = document.createElement('script');
ds.type = 'text/javascript';
ds.async = true;
ds.src = 'http://static.duoshuo.com/embed.js';
ds.charset = 'UTF-8';
(document.getElementsByTagName('head')[0]
|| document.getElementsByTagName('body')[0]).appendChild(ds);
})();
</script>
<!-- Duoshuo Comment END -->
{% endif %}

在该文件中,添加了一个if语句,即只有在pelicanconf.py中对DUOSHUO_SITENAME进行设置后才开启多说评论功能。

添加 DuoShuo_thread.html

1
2
3
<noscript>
Please enable JavaScript to view the comments powered by <a href="http://duoshuo.com/">DuoShuo</a>.
</noscript>

添加这个文件的作用是,当浏览器禁用Javascript后,用户无法看见评论框,而这个代码便是对用户进行提示。

修改 article.html

由于评论功能总是出现在文章末尾,因此应该将评论功能模块放在文章模版末尾,即对article.html进行修改,修改内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% raw %}
{% extends "base.html" %}
{% block title %}{{ article.title|striptags }}{% endblock %}
{% block content %}
<div>
<article class="hentry" role="article">
{% include '_includes/article.html' %}
<footer>
{% include '_includes/article_infos.html' %}
</footer>
</article>

{% if DUOSHUO_SITENAME and SITEURL and article.status != "draft" %}
<section>
<h1>Comments</h1>
<div id="DuoShuoComment" aria-live="polite">
{% include '_includes/DuoShuo_Script.html' %}
{% include '_includes/DuoShuo_thread.html' %}
</div>
</section>
{% endif %}
</div>
{% endblock %}
{% endraw %}

通过以上修改,便可在Pelican博客系统中使用多说评论系统。

HTML 字符实体(Character Entity)

发表于 2013-10-04 | 更新于 2019-04-03 | 分类于 Development

字符实体的由来

在HTML源文件中,有些字符(如<,>,&等)作为特殊字符不能直接使用,如果直接使用的话HTML将不能被正确解析。但如果HTML内容中需要展示这些特殊字符时该怎么处理呢?这就需要使用转义字符串来对其进行转义。

转义字符串(Escape Sequence)也称字符实体(Character Entity)。在HTML中,定义转义字符串的原因有两个:

  • 第一个原因:像“<”和“>”这类符号已经用来表示HTML标签,因此就不能直接当作文本中的符号来使用。为了在HTML文档中使用这些符号,就需要定义它的转义字符串。当解释程序遇到这类字符串时就把它解释为真实的字符。在输入转义字符串时,要严格遵守字母大小写的规则。
  • 第二个原因:有些字符在ASCII字符集中没有定义,因此需要使用转义字符串来表示。

转义字符串的组成

转义字符串(Escape Sequence),即字符实体(Character Entity)分成三部分:

  • 第一部分是一个&符号,英文叫ampersand;
  • 第二部分是字符实体名称(character entity names)或者是#加上实体编号;
  • 第三部分是一个分号。

对于实体编号,采用的是字符的Unicode编码序号,并且可以采用其对应的十进制编码或者十六进制编码进行表示,具体形式为#nnnn或#xhhhh。

例如,要显示小于号(<),如果采用字符实体名称进行表示,就可以写成&lt;;如果采用字符实体编号的形式,由于其Unicode编码为60(对应16进制为x3c),因此可以写成&#60;或者&#x3c;。

同一个符号,可以用“实体名称”和“实体编号”两种方式进行表示,“实体名称”的优势在于便于记忆,但不能保证所有的浏览器都能顺利识别它;而“实体编号”,各种浏览器都能处理,其劣势在于不便于记忆。

提示:实体名称(Entity)是区分大小写的。

如何显示空格?
通常情况下,HTML会自动截去多余的空格,多个空格都将被看做一个空格。为了在网页中增加空格,可以使用&nbsp;表示空格。

HTML Encode & Decode

对于HTML字符实体的Encode和Decode,即是对字符(character)和其对应的字符实体(Character Entity)进行相互转换。

HTML Encode: 字符(Character) => 字符实体(Character Entity)
HTML Decode: 字符实体(Character Entity) => 字符(Character)

例如,对于汉字“测试”,由于其Unicode编码为\u6D4B\u8BD5(对应的十进制为27979 35797),因此Encode后得到&#27979;&#35797;;反向地,对&#27979;&#35797;进行Decode后便得到“测试”。当然,对&#x6D4B;&#x8BD5;进行Decode同样可以得到“测试”。

使用 Pelican + GitCafe Pages 创建 Blog

发表于 2013-10-03 | 更新于 2019-04-03 | 分类于 效率工具

博客系统的技术实现

博客系统采用 Pelican + GitCafe Pages 的技术架构。

其中,Pelican是一个由Python开发的用于生成静态页面的程序,可以将markdown等格式的文本生成模版格式化的html静态页面;而GitCafe提供的Pages服务可以通过项目的形式对Pelican生成的html文件进行托管,并可通过域名绑定的形式实现独立域名。

这种实现方式与 Jekyll + Github Pages 的实现方式大致相同,差别在于不同的静态页面生成系统和不同的托管平台。而为什么选择GitCafe而非GitHub,是因为GitHub服务器在国外,访问速度较慢,且存在偶尔无法访问的情况。

博客系统的初始化

创建 Blog 目录

1
2
mkdir 52test.org
cd 52test.org

初始化 Blog

1
pelican-quickstart

根据提示一步步输入相应的配置项,完成配置后在Blog目录下会生成配置文件pelicanconf.py。但通过pelican-quickstart配置的选项较少,更多的配置选项可以参照Pelican官方网站中的教程对pelicanconf.py进行编辑。

配置GitCafe

在使用GitCafe之前,需要对电脑的Git客户端进行配置,使电脑与远程GitCafe服务器完成认证。
通常地,我们采用SSH Key认证方式,这需要我们在使用GitCafe前须先建创自已的SSH key。

进入SSH目录

1
cd ~/.ssh

如果还没有 ~/.ssh 目录的话,请先手工创建一个 mkdir ~/.ssh

生成新的SSH秘钥

输入下面的代码,将命令中的 YOUR_EMAIL@YOUREMAIL.COM 改为你的Email地址,就可以生成新的key文件。当需要输入文件名的时候,点击回车键,即采用默认设置。

1
2
3
$ ssh-keygen -t rsa -C "YOUR_EMAIL@YOUREMAIL.COM"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/your_user_directory/.ssh/id_rsa):<回车就好>

然后系统会要输入加密串(Passphrase)

1
2
Enter passphrase (empty for no passphrase):<输入加密串>
Enter same passphrase again:<再次输入加密串>

最后看到如下的界面,即表示ssh key成功完成设置。

1
2
3
4
Your identification has been saved in /c/Users/your_user_directory/.ssh/id_rsa.
Your public key has been saved in /c/Users/your_user_directory/.ssh/id_rsa.pub.
The key fingerprint is:
15:81:d2:7a:c6:6c:0f:ec:b0:b6:d4:18:b8:d1:41:48 YOUR_EMAIL@YOUREMAIL.COM

SSH 秘钥生成结束后,可以在用户目录 (~/.ssh/) 下看到私钥 id_rsa 和公钥 id_rsa.pub 这两个文件,请妥善保管这两个文件。

添加 SSH 公钥到 GitCafe

用文本工具打开公钥文件 ~/.ssh/id_rsa.pub ,复制里面的所有内容到剪贴板。
进入 GitCafe->【账户设置】->【SSH 公钥管理】设置项,点击【添加新公钥】按钮,在Title文本框中输入标题,在Key文本框粘贴刚才复制的公钥字符串,输入GitCafe账户密码后按【保存】按钮完成操作。

测试连接

以上步骤完成后,就可以通过以下命令来测试是否可以连接 GitCafe 服务器了。

1
ssh -T git@gitcafe.com

如果是第一次连接的话,会出现以下警告,

1
2
3
The authenticity of host 'gitcafe.com (50.116.2.223)' can't be established.
#RSA key fingerprint is 84:9e:c9:8e:7f:36:28:08:7e:13:bf:43:12:74:11:4e.
#Are you sure you want to continue connecting (yes/no)?

正常情况下,显示的RSA key fingerprint应该与GitCafe提供的公钥一致,即84:9e:c9:8e:7f:36:28:08:7e:13:bf:43:12:74:11:4e。

如果没有问题,输入yes按回车就可以了,中间会提示输入 passphrase 口令。

最后,如果连接成功的话,会出现以下信息。

1
Hi USERNAME! You've successfully authenticated, but GitCafe does not provide shell access.

测试通过后,就可以通过Git客户端向GitCafe服务器上传博客内容了。

撰写文章

在 content 目录下用 Markdown 语法来写一篇文章,格式大致如下所示,文件保存后缀为.md。

1
2
3
4
5
6
Title: My First Blog #标题
Date: 2013-10-05 #日期
Tags: test, blog #标签
Slug: my-first-post #URL

文章内容

生成html静态页面

创建输出目录

1
mkdir leolee

生成页面

1
pelican -s pelicanconf.py content -o leolee

上传到 GitCafe

先到 GitCafe 上创建一个与用户名相同的项目,例如GitCafe帐号名称为leolee,则创建一个名为leolee的项目。

进入静态页面输出目录

1
cd leolee

初始化 git 仓库

1
git init

创建一个gitcafe-pages的分支,并切换到该分支

1
git checkout -b gitcafe-pages

添加所有文件

1
git add .

提交

1
git commit -m "init"

添加远程仓库

1
git remote add origin git@gitcafe.com:leolee/leolee.git

push 到 GitCafe 仓库

1
git push -u origin gitcafe-pages

完成
push 完成以后就可以在访问 GitCafe Pages 地址了。 http://leolee.gitcafe.com

注:不要用 make html 来构建 Blog ,它会删除输出目录后重新生成 Blog ,这意味着会删除 .git 库。

更新博客

为了方便更新博客,可以创建了一个shell脚本,放置在Blog根目录下。在此创建的脚本名称为publish.sh,其内容如下所示。

1
2
3
4
5
6
pelican -s pelicanconf.py content -o leolee
cd leolee
git add .
git commit -m "Update"
git push
cd ../

更新博客时,只需要将新增博客的markdown文件放入content目录中,然后运行如下命令。

1
$ ./publish.sh

当然,在正式发布前通常会先利用本地服务器进行预览,确定没问题后再进行正式发布。

Makefile的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PY=python
PELICAN=pelican
BASEDIR=$(CURDIR)
INPUTDIR=$(BASEDIR)/content
OUTPUTDIR=$(BASEDIR)/output
CONFFILE=$(BASEDIR)/pelicanconf.py
PELICANOPTS=

help:
@echo 'Makefile for a pelican Web site '
@echo ' '
@echo 'Usage: '
@echo ' make clean Empty the OUTPUTDIR '
@echo ' make generate Generate static files for the web site '
@echo ' make serve Serve site at http://localhost:8000 '

clean:
@echo -n 'Cleaning Output folder..............'
@rm -rf $(OUTPUTDIR)/*
@echo ''
@echo 'Done'

generate: clean
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)

serve: generate
cd $(OUTPUTDIR) && $(PY) -m pelican.server

.PHONY: help clean generate serve

要预览时,只需要输入如下命令:

1
$ make serve

然后在浏览器中访问 http://localhost:8000 即可进行本地预览。

参考文章

1
2
[1]: http://riku.gitcafe.com/pelican-gitcafe.html
[2]: https://gitcafe.com/GitCafe/Help/wiki/%E5%A6%82%E4%BD%95%E5%AE%89%E8%A3%85%E5%92%8C%E8%AE%BE%E7%BD%AE-Git#wiki

测试最大在线用户数

发表于 2013-08-22 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

背景介绍

在性能测试工作中,有时需要对业务系统所能支持的最大在线用户数目进行评估。这与我们接触最多的压力测试不一样,因为用户在线时只是与服务器保持连接,并不一定对服务器有业务请求,从而对服务器不一定会产生压力。然而,在线用户数目并非可以无限增长,当在线用户数目达到应用服务器(或者WebLogic等中间件,或者数据库连接池等)的连接数设置的极限时,业务系统同样可能会发生异常,出现新用户无法登录,或者老用户被挤出系统,甚至业务系统宕机的情况。因此,对业务系统的最大在线用户数指标进行测试是极其必要的。

现有一OA系统,需要测试其支持的最大在线用户数目。已知当使用浏览器登录该系统后,登录用户可持续地保持登录状态,即使长时间不做任何操作也不会自动退出系统;通过该OA系统的在线用户数统计模块可以详细地查看到当前在线的用户。

测试方法分析

为了测试被测系统所能支持的最大在线用户数,需要不断地使用新用户帐号进行登录操作,在此同时查看被测系统的在线用户数目以及系统的响应情况。

在新增登录用户时需要注意,由于考察的是系统在正常情况下所能支持的在线用户数目,而不是系统在并发压力下的性能响应情况,因此登录用户时最好采用单个用户或少量并发用户(如两个或三个)逐步登录的形式,不同登录批次之间最好能有一定时间间隔,务必使新增登录用户的操作对服务器产生尽可能小的业务压力。

在新增登录用户的过程中,需要对被测系统的在线用户数目进行查看,并着重关注以下几个方面:

  • 持续新增登录用户的同时,业务系统中的在线用户数目是否相应地进行增长
  • 持续新增登录用户的过程中,系统登录操作是否产生连接超时的情况,事务的响应时间是否出现大幅度上升的情况,系统登录事务是否出现失败的情况(这需要在脚本中对登录事务做检查点设置)
  • 持续新增登录用户的过程中,定期地在浏览器中手动刷新业务系统界面,查看业务系统是否出现不可访问的情况(如内部服务器错误、宕机等)

相应地,测试脚本需符合如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
lr_start_transaction("进入登录页面");
/*此处为进入系统登录界面的脚本*/
lr_end_transaction("进入登录页面", LR_AUTO);

lr_start_transaction("登录系统");

/*登录脚本--部分1*/

/*对系统登录进行校验
*若成功,即新页面中包含"重新登录",reLogin_Count>0
*若失败,reLogin_Count=0
*/
web_reg_find("Text=重新登录",
"SaveCount=reLogin_Count",
LAST );

/*登录脚本--部分2(包含检查点内容部分)*/

//对系统登录结果进行检查
if (atoi(lr_eval_string("{reLogin_Count}")) == 0)
{ //登录失败
lr_error_message("登录失败!!!--UserName:%s",lr_eval_string("{UserName}"));
//勾选Fail Open Transations on lr_error_message
//当执到该lr_error_message时,"登录系统"的Transaction失败
}

lr_end_transaction("登录系统", LR_AUTO);

以上脚本调试成功后,通过检查点函数及日志信息可以判断出系统登录操作已通过脚本回放成功完成。

理论上,只要VuGen采用不同帐号迭代运行该脚本,由于只是进行系统登录操作而未进行系统注销或退出操作,业务系统中的在线用户数将持续增加。

然而在该OA系统中采用此方法时发现,虽然脚本成功运行,但业务系统中的在线用户数并未增长。这说明LoadRunner与浏览器在访问系统的过程中存在差异性。

脚本及场景设计

针对上面的问题,通过抓包工具Fiddler2对系统进行网络流量抓包
分析可知:用户登录系统后,在未进行任何操作的情况下,浏览器与服务器会定期(间隔30秒)进行通讯交互。如下图所示:

Online-Users-Communication-between-Browser-and-Server

这就解释了为什么用户在浏览器中登录系统后可以长期地保持在线,而通过脚本成功地进行系统登录后却无法保持在线状态;因为VuGen不会像浏览器那样定期地与服务器进行通讯交互。

找出其中的差异后,我们便可在VuGen中用脚本模拟浏览器的定期交互功能,简单的实现方法如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while(1){
web_url("userAction.struts",
"URL=http://10.147.15.28:9001/userAction.struts?actionType=refreshDynaInfo",
"Resource=0",
"RecContentType=text/html",
"Referer=http://10.147.15.28:9001/jsp/oa/infocomm/sms/sysShortMsg/sysShortMsgReflesh.jsp",
"Snapshot=t4.inf",
"Mode=HTML",
LAST);

web_url("userAction.struts_2",
"URL=http://10.147.15.28:9001/userAction.struts?actionType=refreshDynaInfo&time=Mon%20Aug%2012%2015:42:04%20UTC+0800%202013",
"Resource=0",
"RecContentType=text/html",
"Referer=http://10.147.15.28:9001/jsp/oa/infocomm/sms/sysShortMsg/reflesh.jsp",
"Snapshot=t5.inf",
"Mode=HTML",
LAST);

lr_think_time(30); //模拟浏览器与服务器30秒间隔的通讯交互
}

那么,将系统登录脚本和循环函数都放入Action中,在VuGen中采用迭代运行的方式可行吗?

虽然新增登录用户可以逐个进行,即使用VuGen通过Action迭代的形式采用不同帐号逐个地进行登录操作。但由于用户登录后需要持续间隔地与服务器进行通讯交互才能保持在线,而单线程脚本运行至while(1)后便进入死循环,从而使得Action迭代无效。因此,采用VuGen进行Action迭代的方式是不可行的。

正确的做法是,在Controller中采用逐步加载的方式,使各个虚拟用户独立地运行,从而保证了各虚拟用户登录成功后保持在线状态。

在Controller中的具体配置如下所示:

  • Run Logic:Number of Iterations设置为1
  • Think Time:Replay think time As recorded
  • Continue on error:False
  • Fail open transactions on lr_error_message:True
  • Schedule:Initialize each Vuser just before it runs
  • Schedule:Start 1000 Vusers – 1 Vuser every 3 Seconds
  • Schedule:Run for 5 minutes

通过以上配置,可以达到如下效果:

  • 每个Vuser只登录一次,然后定期30秒与服务器进行一次交互,保持在线状态
  • Vuser的登录操作与定期刷新操作不会对服务器造成并发压力,且符合真实业务场景
  • 逐步缓慢地增加在线用户数,当系统出现异常时即可查看到当前的在线用户数目

测试方法优化

通过以上方法可以测试得到业务系统所能承受的“初略的”最大在线用户数目。为什么说是“初略的”呢?因为该方法仍存在缺陷,主要体现在如下两个方面:

  • 该方法只适用于测试期间无他人使用系统的情况。如果测试期间同时有其他用户登录系统,或者系统中本身已存在在线用户,则会造成测试得到的结果不准确。
  • 该方法忽略了系统稳定性对在线用户数的影响。举例来说,也许逐步增加在线用户数至500时,系统并没有发生异常,但这并不意味着500个用户长时间处于在线状态时系统不会出现异常。

针对以上两方面缺陷,可以做出如下改进:

  • 在逐步增加在线用户数的时候,定期(比如间隔3秒)查看业务系统自身统计的在线用户数目,并以该数据为测试结果。
  • 利用之前的方法测试得到业务系统“初略的”最大在线用户数后,使系统长时间保持该数量的在线用户数目,观察系统在长时间运行期间是否会出现异常;若出现异常后,适当减少在线用户数目后重复地进行测试,直到系统可以保持长时间地稳定运行为止,此时对应的在线用户数目即为业务系统所能承受的最大在线用户数目。

在本文提到的OA系统中,对链接[http://10.147.15.28:9001/countAction.struts?actionType=listOnlineUser]进行请求可以返回得到当前系统在线用户数目的统计信息。对应地,设计如下脚本,即可实现对系统实时在线用户数目的查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Action()
{

web_add_cookie("USERORGID=db93a2M11f6719ff92Mf528764d624db129b32c21fbca0cb8d6; DOMAIN=10.147.15.28");

while(1){

lr_start_transaction("查看在线用户数目");

web_reg_save_param("OnlineUsers",
"LB=[在线用户数/总用户数:<font color=\"red\">",
"RB=</font>]",
"ORD=2",
"Notfound=error",
"Search=Body",
LAST);

web_url("countAction.struts",
"URL=http://10.147.15.28:9001/countAction.struts?actionType=listOnlineUser",
"Resource=0",
"RecContentType=text/html",
"Referer=",
"Snapshot=t1.inf",
"Mode=HTML",
LAST);

lr_output_message("当前在线用户数目:%s", lr_eval_string("{OnlineUsers}"));

lr_end_transaction("查看在线用户数目", LR_AUTO);

lr_think_time(3);

}

return 0;
}

LoadRuner 中的参数与变量

发表于 2013-08-12 | 更新于 2019-04-03 | 分类于 Testing , 性能测试

在LoadRunner脚本开发中,经常会遇到参数与变量相互转换的情况,本文对常见的转换情形进行了方法总结。

变量的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//将字符串赋值给变量
char strTemp[30];
strcpy(strTemp, "Hello World!!");

//错误的字符串赋值方式
strTemp = "Hello World!!";
/* 注:
* 在LR中若直接将字符串赋值给变量,编译时将会报错
* 报错信息:operands of = have illegal types `char' and `pointer to char'
*/

//将数值赋值给变量
int x = 10;
/* 注:
* 在LR中,变量的声明一定要放在脚本的最前面,且声明的语句中不要有其他的脚本代码
* 若将以上申明放置在脚本中部,将会产生如下形式的报错信息
* illegal statement termination
* skipping 'int'
* undeclared identifier 'x'
*/

参数的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//将字符串赋值给参数
lr_save_string("Hello World!!","paraStr");

//将变量中的值赋值给参数
char strTemp[30];
strcpy(strTemp, "Hello World") ;
lr_save_string(strTemp, "paraStr");

//将数值直接赋值给参数
lr_save_int(123, "paraNum");

//将变量中的数值赋值给参数
int num = 10;
lr_save_int(num*2, "paraNum");

参数的取值

1
2
3
4
5
//从参数中进行取值,不管参数是字符串还是数值
lr_eval_string("{paraStr}");
lr_eval_string("{paraNum}");
//取出的值均为字符串类型,因此输出时格式需为"%s"
lr_output_message("%s", lr_eval_string("{paraNum}"));

参数=>变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//将参数转换为字符串变量,参数paraStr中的值为"Hello World!!"
char strTemp[30];
strcpy(strTemp, lr_eval_string("{paraStr}"));
lr_output_message("%s", strTemp);

//将参数转换为数值变量,参数paraNum中的值为"246"
int num;
num = atoi(lr_eval_string("{paraNum}")); //将字符串转换为数值
lr_output_message("%d", num);

//将参数格式化输出到变量
SeatPrefListCount = atoi( lr_eval_string("{SeatPrefList_count}") );
sprintf(varRandomSeatPref, "{SeatPrefList_%d}", 1+rand()%SeatPrefListCount);
//将格式化的随机日期写入变量varRandomDepartDate
sprintf(varRandomDepartDate, "%d/%d/%d", 1+rand()%12, 1+rand()%28, 2009+rand()%6);

参数=>参数

1
2
//参数的复制:将参数paraStr_1的值复制到参数paraStr_2
lr_save_string(lr_eval_string("{paraStr_1}"),"paraStr_2");
1…678
solomiss

solomiss

75 日志
11 分类
50 标签
GitHub E-Mail
Creative Commons
© 2019 solomiss