ITester接口测试框架
ITester接口测试框架
本系列教材将主要为大家讲解接口 框架的设计,本框架属于轻量型的,目前的需求基本都能满足,后续还要不断的完善,最后开源出去。暂且命名为ITester(取名字是一件很蛋疼的事情,要与接口相关,又要不能和其他框架重名)
同样先规划下系列blog目录:
ITester接口测试框架(一):接口基础知识
ITester接口测试框架(二):框架简介和流程图
ITester接口测试框架(三):框架实现细节
接下来开始第一部分,接口测试基础知识的讲解
什么是接口测试
接口是指系统模块与模块或系统与系统间进行交互,一般我们用的多的是HTTP协议的接口、WebService协议的接口,还有RPC (Remote Procedure Call Protocol)——远程过程调用协议的接口。
接口测试是为了测试接口,尤其是那些与系统相关联的外部接口,测试的重点是要检查数据的交换,传递和控制管理过程,还包括处理的次数。
不管是哪种接口,其本质就是发送一个request,然后服务器响应 后返回一个response,然后我们对response进行分析,这即是接口测试。
为什么要做接口测试
- 首先,随着系统复杂程度的上升,传统的测试方法测试成本急剧增加,测试效率大幅下降(数据模型推算,底层的一个bug能够引发上层的 8 个左右bug,而且底层的bug很容易引起全网的宕机。相反接口测试能够提供系统复杂度上升情况下的低成本高效率的解决方案。
- 其次接口测试不同于传统开发的单元测试,接口测试是站在用户的角度对系统接口进行全面高效持续的检测。
- 最后接口测试是自动化并且持续集成的,这也是为什么接口测试能够低成本高收益的根源。
接口测试的核心战略在于:以保证系统的正确和稳定为核心,以持续集成为手段,提高测试效率,提升用户体验,降低产品研发成本。
接口测试要为代码的编写保驾护航,增强开发人员和测试人员的自信,让隐含的 BUG提前暴露出来,要让开发人员在第一时间修复 BUG,要让功能测试人员和性能测试人员在测试的时候更加顺手,最大限度得减少底层 BUG 的出现数量,要让产品研发的流程更加 ,要缩短产品的研发周期,最后在产品上线以后,要让用户用得更加顺畅,要让用户感觉产品服务零缺陷。
另外在这个过程中,我们需要几类资源作为支撑,下面做简单描述。
首先最重要的一点是要强调团队的重要性,特别是团队中需要有合理的人力资源配置,在这个团队中,需要全才,也需要专才,需要技术专家,也需要业务专家,既需要高效的执行者,也需要有效的管理者,任何人在这个团队中都可以发挥重要作用。
其次我们需要强大的测试技术以及测试框架去支撑我们的日常工作,包括基于 以及基于 C++的测试框架,甚至以后会扩展到其他各个语种的框架,计算机软件的 发展到今天,特别是分布式软件的发展,导致软件体系结构日益复杂化,各个系统之间的依赖逐渐加强,JAVA、C++以及多种技术的综合使用,使传统的单元测试已经无法满足于针对接口编程的架构方式,我们需要以一种更加干净的层面也就是从业务的层面对接口进行隔离测试,同时为了模拟真实场景,也需要在真实的环境中对系统内根据业务流程对各个接口进行串联测试,
最后以持续集成系统保证被测代码的稳定性。再次要充分重视文档的重要性,包括需求文档,开发技术方案,测试技术方案,接口定义 JAVADOC,测试用例文档等等,完善这些文档可以大大减少软件工程周期中各个团队配合障碍,也可以降低后期软件维护成本。因此贯彻和落实接口测试的战略可以最大程度地提高软件质量的稳定性。
接口的种类
- 系统与系统之间的调用,比如支付宝会提供接口给DJI商城调用,或者说,支付宝会提供接口给天猫调用
- 上层服务对下层服务的调用,比如Service层会调用DAO层的接口,而应用层又会调用Service层提供的接口
- 服务之间的调用,比如注册用户时,会先调用用户查询的服务,查看该用户是否已经注册。
而我们所要做的接口测试,先要了解是基于哪一种类型的接口测试,不同类型的接口测试方法可能是不一致的,总体来说,不管是哪种类型,我们只要把被测接口当做是服务方,而把我们的测试手段当做是客户方,我们的目的就是,通过我们的测试手段,去验证服务端满足了他声明提供的功能。
GET和POST区别
参考我之前的blog:
GET方式提交
a)地址栏(URI)会跟上参数数据。以?开头,多个参数之间以&分割。
GET /day09/testMethod.html?name=eric&password=123456 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,en-us;q=0.8,zh;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/day09/testMethod.html
Connection: keep-alive
b)GET提交参数数据有限制,不超过1KB。
c)GET方式不适合提交敏感密码。
d)注意: 浏览器直接访问的请求,默认提交方式是GET方式
POST方式提交
a)参数不会跟着URI后面。参数而是跟在请求的实体内容中。没有?开头,多个参数之间以&分割。
POST /day09/testMethod.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,en-us;q=0.8,zh;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/day09/testMethod.html
Connection: keep-alive
name=eric&password=123456
b)POST提交的参数数据没有限制。
c)POST方式提交敏感数据。
接口测试的方法
至于接口测试具体的测试方法,http协议的接口测试,一般会用jmeter去测试,jmeter的好处是不用写测试代码,直接使用jmeter提供的http请求去测试,也可以使用postman,也可以使用HttpClient去测试,好处是可以方便集成和自动化。java接口的测试,则需要编写测试代码去测试,有点类似于单元测试。
但是以上的方法都只是通过发送Request,返回Respouse,简单的验证返回值是否正确,它的缺点很多:
它不能将请求参数完全的覆盖,只是简单的覆盖几个参数
手动的一条一条伪造自测数据
对于复杂类型的请求参数,例如javaBean对象,无法发送
不能自动化的判断返回值是否正确
需要手工一条一条去执行接口,效率太低
基于以上缺点,github上有很多同学做了各种不同的接口框架,比如百度的iTest,阿里taobao etao测试团队开发的TOAST,网易的Dagger,但是上述框架都是基于自己公司的业务基础设计开发的,只供参考,我自己设计了一套接口测试框架 ,暂且命名为ITester,详细介绍请看下一篇博文。
ITester框架简介
上一篇的blog主要讲解了接口 的基础知识,这一章将详细介绍接口测试框架图和流程图。
ITester是处理API接口测试的轻量级自动化测试框架, 语言实现。主要是面向service接口,诸如:HTTP,SOAP,JSON-RPC的轻量级自动化测试框架。将数据准备、参数的输入、请求执行、结果解析、数据验证、数据清理各个环节都做了封装,以数据文本文件为接口,降低自动化测试过程中case书写的难度。底层主要使用的是HttpClient+TestNG
工具特点:
- 采取数据驱动方式,支持Excel 和Xml以及数据库的方式录入接口请求数据;
- 支持失败用例重跑;
- 支持用例多线程执行,缩短执行时间(用例间需线程安全);
- 接入Jenkins持续集成平台,随时随地自动化构建,执行测试;
- 支持定制化测试报告,邮件通知测试结果。
ITester框架图
如图所示,我将框架分为五大部分:
- Client:也就是数据准备、参数的输入、请求执行、结果解析、数据验证、数据清理各个环节
- TestCase:测试脚本用例模块
- CI:也就是持续集成模块
- Report:测试报告和邮件通知
- 调度:触发整个测试框架运行起来
ITerster组件
核心组件
- 通过Maven进行项目和依赖管理
- 通过HttpClient和服务端进行通信
- 通过TestNG进行测试脚本的管理
- 通过Log4j生成运行日志
- 通过Jackson实现各类型数据:javaBean,xml,json之间的相互转换
- 通过C3P0数据库连接池实现与数据库的连接通信
- 通过AssertJ提供丰富的断言
- 通过ReportNG输出丰富多彩的测试报告
- 最后将源码托管于Git通过Jenkins实现持续集成
调度执行
- 通过Maven进行项目的构建,测试
- 通过Shell脚本调度执行
- 通过Jenkins平台实现持续集成
接口调用流程
用例执行流程
下一篇blog将主要介绍ITester接口框架的实现细节,后续相关的文档和源码会开源到github上!
项目结构
结构图
框架实现
1.数据准备 和 参数输入
数据准备有三种方式:
- 一:通过访问数据库,获取我们需要的接口测试参数,比如测试登录接口,我们要提前获得 用户名和密码,然后才能连同我们的url发送给服务端。
要想访问 ,必须要先与数据库建立连接,我这里采用的是JDBC 和C3P0,可以参考我之前的blog
- 二:直接将所需要的数据写入到xml或者excel中,然后写一个工具类直接去读取就行。
例如 :要准备 接口输入参数为:email和password
(1)编写paramsData.xml,将上述参数放在xml中进行管理
<?xml version="1.0" encoding="UTF-8"?>
<map>
<bean beanName="signIn">
<locator name="email" value="charlie.chen@dji.com"></locator>
<locator name="password" value="123456"></locator>
</bean>
</map>
(2)封装一个XmlUtil工具类负责读取xml ,使用第三方的jar包dom4j,XmlUtil中readXMLDocument方法返回值为Map
public static Map<String, String> readXMLDocument(String xmlPath, String beanName) throws IOException {
}
上述readXMLDocument方法中,参数xmlName为xml文件的名字; 参数beanName为xml文件中节点的名称。
- 三:通过xml管理测试数据,也可以用Excel进行管理
和xml管理数据类似:封装一个ExcelUtil工具类负责读取excel ,使用第三方的jar包poi,ExcelUtil中readExcel方法返回值为List
// 读取Excel中数据
public static List<ParamBean> readExcel(String excelPath) throws Exception {
}
2.请求执行
(1)封装一个CookieUtil工具类,通过CookieStore储存cookie
CookieUtil类中setCookieStore方法返回值为CookieStore
**
* 将服务端返回的SessionID存储在CookieStore中
* @author Charlie.chen
* @date 2016-10-31
*
*/
public class CookieUtil {
private static CookieStore cookieStore = null;
private static LogUtil log = new LogUtil(CookieUtil.class);
/*
* 通过CookieStore储存cookie
*/
public static CookieStore setCookieStore(HttpResponse httpResponse) {
log.info("setCookieStore");
cookieStore = new BasicCookieStore();
// JSESSIONID
String setCookie = httpResponse.getFirstHeader("Set-Cookie").getValue();
String JSESSIONID = setCookie.substring("JSESSIONID=".length(),
setCookie.indexOf(";"));
log.debug("JSESSIONID:" + JSESSIONID);
// 新建一个Cookie
BasicClientCookie cookie = new BasicClientCookie("JSESSIONID", JSESSIONID);
cookie.setVersion(0);
cookie.setDomain("127.0.0.1");
cookie.setPath("/CwlProClient");
cookieStore.addCookie(cookie);
return cookieStore;
}
}
上述setCookieStore方法中,httpResponse参数为服务端响应值,类型为HttpResponse
(2)用httpClient简单封装一个httpClientUtil工具类有get.post,put,delete方法
public static CloseableHttpResponse doGet(String url, Map<String, String> paramsMap, CloseableHttpClient httpclient, CookieStore cookieStore) {
}
public static CloseableHttpResponse doPost(String url, Map<String, String> paramsMap, CloseableHttpClient httpclient,CookieStore cookieStore) {
}
public static CloseableHttpResponse doPut(String url, Map<String, String> paramsMap, CloseableHttpClient httpclient, CookieStore cookieStore) {
}
public static CloseableHttpResponse doDelete(String url, Map<String, String> paramsMap, CloseableHttpClient httpclient, CookieStore cookieStore) {
}
/**
* 封装一个获取请求实体的方法
* @param params
* @param ucode
* @return
*/
public static UrlEncodedFormEntity getFormEntity(Map<String, String> paramsMap, Charset... ucode) {
}
3.结果解析 和 验证
(1)将从服务端返回的HttpResponse结果解析为ResponseBean对象
public class ReponseUtil {
private static LogUtil log = new LogUtil(ReponseUtil.class);
private static ResponseBean responseBean=null;
public static ResponseBean setResponseBean(CloseableHttpResponse httpResponse) {
// 使用响应对象获取响应实体
HttpEntity entity = httpResponse.getEntity();
if (entity != null)
try {
// 将响应实体转为字符串
String responseString = EntityUtils.toString(entity, "utf-8");
String rs = responseString.replace("\r\n", "");
responseBean = new ResponseBean();
responseBean.setStatus(httpResponse.getStatusLine().getReasonPhrase());
responseBean.setStatusCode(Integer.toString(httpResponse.getStatusLine().getStatusCode()));
responseBean.setBody(rs);
log.info("\n" + "***************************返回开始**********************************" + "\n"
+ httpResponse.getStatusLine().getReasonPhrase() + "\n"
+ Integer.toString(httpResponse.getStatusLine().getStatusCode()) + "\n" + "Context" + rs + "\n"
+ "***************************返回结束**********************************");
HeaderIterator iterator = httpResponse.headerIterator();
while (iterator.hasNext()) {
log.debug("\t" + iterator.next());
}
} catch (Exception e) {
e.printStackTrace();
}
return responseBean;
}
}
(2)结果验证
主要验证 HttpResponse的状态status,状态码statusCode和 响应实体body,通过第三方断言jar包AsertJ
// add Assert
Assert.assertEquals("OK", responseBean.getStatus());
Assert.assertEquals("200", responseBean.getStatusCode());
Assert.assertEquals("dsgfdfgdfsdfdgfdg", responseBean.getBody());
4.测试用例
测试用例管理使用了testNG管理 ,使用了TestNG参数化测试,通过xml文件来执行case
(1)测试case脚本
public class NewTest {
static CookieStore cookieStore=null;
static CloseableHttpClient httpclient=null;
@Test
public void test() {
try {
String url = PropertiesUtil.getValue("url","config.properties");
String xmlPath=NewTest.class.getClassLoader().getResource("paramData.xml").getPath();
Map<String, String> paramsMap = XmlUtil.readXMLDocument(xmlPath, "signIn");
httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
CloseableHttpResponse httpResponse = HttpClientUtil.doPost(url, paramsMap, httpclient, cookieStore);
ResponseBean responseBean = ReponseUtil.setResponseBean(httpResponse);
// add Assert
Assert.assertEquals("OK", responseBean.getStatus());
Assert.assertEquals("200", responseBean.getStatusCode());
Assert.assertEquals("dsgfdfgdfsdfdgfdg", responseBean.getBody());
} catch (IOException e) {
e.printStackTrace();
}
}
@BeforeClass
public void beforeClass() {
}
@AfterClass
public void afterClass() {
}
@BeforeTest
public void beforeTest() {
}
@AfterTest
public void afterTest() {
}
@BeforeSuite
public void beforeSuite() {
/*
* 登录进入系统获取JSESSIONID放入到CookieStore中
* */
}
@AfterSuite
public void closeClient() {
try {
// 关闭流并释放资源
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)testng.xml文件的编写
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="false" preserve-order="true">
<test name="Test">
<classes>
<class name="com.dji.itester.cases.NewTest">
<methods>
<include name="test" />
</methods>
</class>
</classes>
</test>
</suite>
右键->run as ->TestNG Suite,这个场景的的测试用例就可以运行了
5.失败用例重跑
关于失败用例重跑,在我前面的blog 写的很清楚了,这里就不详说了,大概步骤如下:
(1)新建TestNGRetry类继承IRetryAnalyzer,实现用例失败自动重跑逻辑
(2)添加用例重跑监听器RetryListener,用例失败自动重跑功能
(3)在testng.xml文件中配置自己编写的监听器
<listeners>
<listener class-name="com.dji.itester.runfail.TestNGListener" />
<listener class-name="com.dji.itester.runfail.RetryListener"/>
</listeners>
6.源码管理 和 Jenkins配置
(1)源码管理
将代码上传到github上进行托管,关于github的使用参考我前面的blog )
(2)Jenkins配置
运行jenkins,首先创建一个job为ITester-DJI
将代码上传到github上进行托管,然后在jenkins上配置clone 到你本地或者远程的jenkins来:
构建触发器
Poll SCM: 定时检查源码变更(根据SCM软件的版本号),如果有更新就checkout最新code下来,然后执行构建动作。Build periodically:周期进行项目构建(它不care源码是否发生变化)
我的配置如下: 每天凌晨2:00进行一次构建
构建
构建后操作,发送邮件
7.测试报告和邮件
这里用到第三方的jar包ReportNG,对测试报告进行定制化。添加插件,关联testNg.xml,添加ReportNg的监听器,修改最后的TestNG的报告。
<build>
<plugins>
<!-- 添加插件,关联testNg.xml,添加ReportNg的监听器,修改最后的TestNG的报告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<forkMode>once</forkMode>
<argLine>-Dfile.encoding=UTF-8</argLine>
<suiteXmlFiles>
<!--<suiteXmlFile>testng.xml</suiteXmlFile> -->
<suiteXmlFile>src/test/java/com/dji/itester/testSuites/${xmlFileName}</suiteXmlFile>
</suiteXmlFiles>
<properties>
<property>
<name>usedefaultlisteners</name>
<value>false</value>
</property>
<property>
<name>listener</name>
<value>org.uncommons.reportng.HTMLReporter,
org.uncommons.reportng.JUnitXMLReporter</value>
</property>
</properties>
<workingDirectory>target/</workingDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
测试报告:
转自:http://blog.csdn.net/ToBeTheEnder/article/category/6372155