创建项目引入依赖
一、创建maven项目
二、引入依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor 日志拦截器-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>3.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.testng/testng -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.2</version>
<!--<scope>test</scope>-->
<!--作用范围,默认是test。验证部分被抽象,不仅test作用域需使用-->
</dependency>
<!-- https://mvnrepository.com/artifact/com.aventstack/extentreports -->
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>3.1.5</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.vimalselvam/testng-extentsreport -->
<dependency>
<groupId>com.vimalselvam</groupId>
<artifactId>testng-extentsreport</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson 对象转换器-->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<!--为了maven打包编译时后台一直输出警告信息。导致构建失败-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<xmlFileName></xmlFileName>
</properties>
多环境切换配置
三、配置多环境切换
配置pom.xml文件的 build节点
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<!--扫描替换参数的文件路径-->
</resource>
</resources>
<filters>
<filter>src/main/filters/filter-${env}.properties</filter>
<!--环境过滤器的配置方式,回头需要在该路径下建立对应文件-->
</filters>
<plugins>
<plugin>
<!--该插件是解决命令下执行mvn test指定testng xxx.xml 文件 的配置-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<!--为了解决在jenkins maven执行test 报告乱码问题,编码格式设置为UTF-8-->
<argLine>-Dfile.encoding=UTF-8</argLine>
<encoding>UTF-8</encoding>
<!--动态指定执行的xml文件。${project.basedir}项目目录,${xmlFileName}maven文件-->
<suiteXmlFiles>
<suiteXmlFile>${project.basedir}/target/classes/testNg/${xmlFileName}</suiteXmlFile>
<!--在IEDA上运行maven test命令时,可打开该注释使用完整测试集合-->
<!--<suiteXmlFile>${project.basedir}/target/classes/testNg/api/APICollection-TestSuite.xml</suiteXmlFile>-->
</suiteXmlFiles>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
四、在pom.xml文件配置Properties环境,多环境配置参数切换。
<profiles>
<!-- 开发环境,默认激活 -->
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
<!-- 生产环境 -->
<profile>
<id>product</id>
<properties>
<env>product</env>
</properties>
</profile>
<!-- 测试环境 -->
<profile>
<id>debug</id>
<properties>
<env>debug</env>
</properties>
<activation>
<activeByDefault>true</activeByDefault><!--默认启用的是dev环境配置-->
</activation>
</profile>
</profiles>
五、在src/resource下创建env.properties文件,该文件记录的信息是跟环境切换相关的参数。
#Environment
Environment=${Environment}
douban.host=${douban.host}
六、在src/main/filters/下创建filter-debug.properties、filter-dev.properties、filter-product.properties用户记录环境信息参数,模板如下
#Environment
Environment=Debug
douban.host=https://movie.douban.com/
配置testNG
七、在src/resources/下创建testNg目录,存放测试集合的目录,为了能够让所有接口统一运行测试,需建立一个所有的测试集合。
APICollection-TestSuite.xml
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="接口测试集合" verbose="1" preserve-order="true">
<!-- 自定义参数,用于报告展示-->
<parameter name="report.config" value="src/main/resources/config/report/extent-config.xml"/>
<parameter name="system.info" value="reporter.config.MySystemInfo"/>
<!-- 执行的测试用例文件-->
<suite-files>
<suite-file path="search/search-TestSuite.xml"/>
</suite-files>
<!-- 报告监听器配置-->
<listeners>
<listener class-name="reporter.listener.MyExtentTestNgFormatter"/>
</listeners>
</suite>
search-TestSuite.xml
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="search_tags-搜索分类-电影首页测试集合" verbose="1" preserve-order="true">
<parameter name="report.config" value="src/main/resources/config/report/extent-config.xml"/>
<parameter name="system.info" value="reporter.config.MySystemInfo"/>
<test name="0100.搜索分类-电影首页-正常场景" preserve-order="true">
<classes>
<class name="com.jxq.douban.SearchTagsTest">
<methods>
<include name="testcase1"/>
</methods>
</class>
</classes>
</test>
<test name="0100.搜索分类-TV首页-正常场景" preserve-order="true">
<classes>
<class name="com.jxq.douban.SearchTagsTest">
<methods>
<include name="testcase2"/>
</methods>
</class>
</classes>
</test>
<listeners>
<listener class-name="reporter.listener.MyExtentTestNgFormatter"/>
</listeners>
</suite>
配置ExtentReports
八、强制重写ExtentTestNgFormatter类
强制重写EExtentTestNgFormatter类主要是以下两点原因:
①、为了能够在报告中展示更多状态类型的测试结果,例如:成功、失败、警告、跳过等测试状态结果。 ②、因为不支持cdn.rawgit.com访问,故替css访问方式。
在src/main/java/reporter/listener路径下创建MyExtentTestNgFormatter.java
package reporter.listener;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.ResourceCDN;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.vimalselvam.testng.EmailReporter;
import com.vimalselvam.testng.NodeName;
import com.vimalselvam.testng.SystemInfo;
import com.vimalselvam.testng.listener.ExtentTestNgFormatter;
import org.testng.*;
import org.testng.xml.XmlSuite;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
private static final String REPORTER_ATTR = "extentTestNgReporter";
private static final String SUITE_ATTR = "extentTestNgSuite";
private ExtentReports reporter;
private List<String> testRunnerOutput;
private Map<String, String> systemInfo;
private ExtentHtmlReporter htmlReporter;
private static ExtentTestNgFormatter instance;
public MyExtentTestNgFormatter() {
setInstance(this);
testRunnerOutput = new ArrayList<>();
String reportPathStr = System.getProperty("reportPath");
File reportPath;
try {
reportPath = new File(reportPathStr);
} catch (NullPointerException e) {
reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
}
if (!reportPath.exists()) {
if (!reportPath.mkdirs()) {
throw new RuntimeException("Failed to create output run directory");
}
}
File reportFile = new File(reportPath, "report.html");
File emailReportFile = new File(reportPath, "emailable-report.html");
htmlReporter = new ExtentHtmlReporter(reportFile);
EmailReporter emailReporter = new EmailReporter(emailReportFile);
reporter = new ExtentReports();
// 如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
reporter.attachReporter(htmlReporter, emailReporter);
}
/**
* Gets the instance of the {@link ExtentTestNgFormatter}
*
* @return The instance of the {@link ExtentTestNgFormatter}
*/
public static ExtentTestNgFormatter getInstance() {
return instance;
}
private static void setInstance(ExtentTestNgFormatter formatter) {
instance = formatter;
}
/**
* Gets the system information map
*
* @return The system information map
*/
public Map<String, String> getSystemInfo() {
return systemInfo;
}
/**
* Sets the system information
*
* @param systemInfo The system information map
*/
public void setSystemInfo(Map<String, String> systemInfo) {
this.systemInfo = systemInfo;
}
public void onStart(ISuite iSuite) {
if (iSuite.getXmlSuite().getTests().size() > 0) {
ExtentTest suite = reporter.createTest(iSuite.getName());
String configFile = iSuite.getParameter("report.config");
if (!Strings.isNullOrEmpty(configFile)) {
htmlReporter.loadXMLConfig(configFile);
}
String systemInfoCustomImplName = iSuite.getParameter("system.info");
if (!Strings.isNullOrEmpty(systemInfoCustomImplName)) {
generateSystemInfo(systemInfoCustomImplName);
}
iSuite.setAttribute(REPORTER_ATTR, reporter);
iSuite.setAttribute(SUITE_ATTR, suite);
}
}
private void generateSystemInfo(String systemInfoCustomImplName) {
try {
Class<?> systemInfoCustomImplClazz = Class.forName(systemInfoCustomImplName);
if (!SystemInfo.class.isAssignableFrom(systemInfoCustomImplClazz)) {
throw new IllegalArgumentException("The given system.info class name <" + systemInfoCustomImplName +
"> should implement the interface <" + SystemInfo.class.getName() + ">");
}
SystemInfo t = (SystemInfo) systemInfoCustomImplClazz.newInstance();
setSystemInfo(t.getSystemInfo());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
public void onFinish(ISuite iSuite) {
}
public void onTestStart(ITestResult iTestResult) {
MyReporter.setTestName(iTestResult.getName());
}
public void onTestSuccess(ITestResult iTestResult) {
}
public void onTestFailure(ITestResult iTestResult) {
}
public void onTestSkipped(ITestResult iTestResult) {
}
public void onTestFailedButWithinSuccessPercentage(ITestResult iTestResult) {
}
public void onStart(ITestContext iTestContext) {
ISuite iSuite = iTestContext.getSuite();
ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
ExtentTest testContext = suite.createNode(iTestContext.getName());
// 自定义报告
// 将MyReporter.report静态引用赋值为testContext。
// testContext是@Test每个测试用例时需要的。report.log可以跟随具体的测试用例。另请查阅源码。
MyReporter.report = testContext;
iTestContext.setAttribute("testContext", testContext);
}
public void onFinish(ITestContext iTestContext) {
ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
if (iTestContext.getFailedTests().size() > 0) {
testContext.fail("Failed");
} else if (iTestContext.getSkippedTests().size() > 0) {
testContext.skip("Skipped");
} else {
testContext.pass("Passed");
}
}
public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
if (iInvokedMethod.isTestMethod()) {
ITestContext iTestContext = iTestResult.getTestContext();
ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
ExtentTest test = testContext.createNode(iTestResult.getName(), iInvokedMethod.getTestMethod().getDescription());
iTestResult.setAttribute("test", test);
}
}
public void afterInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
if (iInvokedMethod.isTestMethod()) {
ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
List<String> logs = Reporter.getOutput(iTestResult);
for (String log : logs) {
test.info(log);
}
int status = iTestResult.getStatus();
if (ITestResult.SUCCESS == status) {
test.pass("Passed");
} else if (ITestResult.FAILURE == status) {
test.fail(iTestResult.getThrowable());
} else {
test.skip("Skipped");
}
for (String group : iInvokedMethod.getTestMethod().getGroups()) {
test.assignCategory(group);
}
}
}
/**
* Adds a screen shot image file to the report. This method should be used only in the configuration method
* and the {@link ITestResult} is the mandatory parameter
*
* @param iTestResult The {@link ITestResult} object
* @param filePath The image file path
* @throws IOException {@link IOException}
*/
public void addScreenCaptureFromPath(ITestResult iTestResult, String filePath) throws IOException {
ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
test.addScreenCaptureFromPath(filePath);
}
/**
* Adds a screen shot image file to the report. This method should be used only in the
* {@link org.testng.annotations.Test} annotated method
*
* @param filePath The image file path
* @throws IOException {@link IOException}
*/
public void addScreenCaptureFromPath(String filePath) throws IOException {
ITestResult iTestResult = Reporter.getCurrentTestResult();
Preconditions.checkState(iTestResult != null);
ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
test.addScreenCaptureFromPath(filePath);
}
/**
* Sets the test runner output
*
* @param message The message to be logged
*/
public void setTestRunnerOutput(String message) {
testRunnerOutput.add(message);
}
public void generateReport(List<XmlSuite> list, List<ISuite> list1, String s) {
if (getSystemInfo() != null) {
for (Map.Entry<String, String> entry : getSystemInfo().entrySet()) {
reporter.setSystemInfo(entry.getKey(), entry.getValue());
}
}
reporter.setTestRunnerOutput(testRunnerOutput);
reporter.flush();
}
/**
* Adds the new node to the test. The node name should have been set already using {@link NodeName}
*/
public void addNewNodeToTest() {
addNewNodeToTest(NodeName.getNodeName());
}
/**
* Adds the new node to the test with the given node name.
*
* @param nodeName The name of the node to be created
*/
public void addNewNodeToTest(String nodeName) {
addNewNode("test", nodeName);
}
/**
* Adds a new node to the suite. The node name should have been set already using {@link NodeName}
*/
public void addNewNodeToSuite() {
addNewNodeToSuite(NodeName.getNodeName());
}
/**
* Adds a new node to the suite with the given node name
*
* @param nodeName The name of the node to be created
*/
public void addNewNodeToSuite(String nodeName) {
addNewNode(SUITE_ATTR, nodeName);
}
private void addNewNode(String parent, String nodeName) {
ITestResult result = Reporter.getCurrentTestResult();
Preconditions.checkState(result != null);
ExtentTest parentNode = (ExtentTest) result.getAttribute(parent);
ExtentTest childNode = parentNode.createNode(nodeName);
result.setAttribute(nodeName, childNode);
}
/**
* Adds a info log message to the node. The node name should have been set already using {@link NodeName}
*
* @param logMessage The log message string
*/
public void addInfoLogToNode(String logMessage) {
addInfoLogToNode(logMessage, NodeName.getNodeName());
}
/**
* Adds a info log message to the node
*
* @param logMessage The log message string
* @param nodeName The name of the node
*/
public void addInfoLogToNode(String logMessage, String nodeName) {
ITestResult result = Reporter.getCurrentTestResult();
Preconditions.checkState(result != null);
ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
test.info(logMessage);
}
/**
* Marks the node as failed. The node name should have been set already using {@link NodeName}
*
* @param t The {@link Throwable} object
*/
public void failTheNode(Throwable t) {
failTheNode(NodeName.getNodeName(), t);
}
/**
* Marks the given node as failed
*
* @param nodeName The name of the node
* @param t The {@link Throwable} object
*/
public void failTheNode(String nodeName, Throwable t) {
ITestResult result = Reporter.getCurrentTestResult();
Preconditions.checkState(result != null);
ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
test.fail(t);
}
/**
* Marks the node as failed. The node name should have been set already using {@link NodeName}
*
* @param logMessage The message to be logged
*/
public void failTheNode(String logMessage) {
failTheNode(NodeName.getNodeName(), logMessage);
}
/**
* Marks the given node as failed
*
* @param nodeName The name of the node
* @param logMessage The message to be logged
*/
public void failTheNode(String nodeName, String logMessage) {
ITestResult result = Reporter.getCurrentTestResult();
Preconditions.checkState(result != null);
ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
test.fail(logMessage);
}
}
同级目录再创建MyReporter.java,然后注意MyExtentTestNgFormatter.java中使用MyReporter的自定义部分
public class MyReporter {
public static ExtentTest report;
}
导入MyExtentTestNgFormatter监听类
<listeners>
<listener class-name="reporter.Listener.MyExtentTestNgFormatter"/>
</listeners>
九、修改测试报告生成目录(可选),默认生成在test-output/文件夹下,名为report.html、emailable-report.html。
public MyExtentTestNgFormatter() {
setInstance(this);
testRunnerOutput = new ArrayList<>();
// reportPath报告路径
String reportPathStr = System.getProperty("reportPath");
File reportPath;
try {
reportPath = new File(reportPathStr);
} catch (NullPointerException e) {
reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
}
if (!reportPath.exists()) {
if (!reportPath.mkdirs()) {
throw new RuntimeException("Failed to create output run directory");
}
}
// 报告名report.html
File reportFile = new File(reportPath, "report.html");
// 邮件报告名emailable-report.html
File emailReportFile = new File(reportPath, "emailable-report.html");
htmlReporter = new ExtentHtmlReporter(reportFile);
EmailReporter emailReporter = new EmailReporter(emailReportFile);
reporter = new ExtentReports();
reporter.attachReporter(htmlReporter, emailReporter);
}
十、report.log增加状态、耗时记录等
// 根据状态不同添加报告。型如警告
public class MyInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
if (time > 100) {
MyReporter.report.log(Status.WARNING, MyReporter.getTestName() + " 接口耗时:" + time);
}
return response;
}
}
十一、配置报告信息
extent reporters支持报告的配置,目前支持的配置内容有title、主题等。
在src/resources/目录下添加 config/report/extent-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<extentreports>
<configuration>
<timeStampFormat>yyyy-MM-dd HH:mm:ss</timeStampFormat>
<!-- report theme -->
<!-- standard, dark -->
<theme>dark</theme>
<!-- document encoding -->
<!-- defaults to UTF-8 -->
<encoding>UTF-8</encoding>
<!-- protocol for script and stylesheets -->
<!-- defaults to https -->
<protocol>https</protocol>
<!-- title of the document -->
<documentTitle>QA-接口自动化测试报告</documentTitle>
<!-- report name - displayed at top-nav -->
<reportName>QA-接口自动化测试报告</reportName>
<!-- report headline - displayed at top-nav, after reportHeadline -->
<reportHeadline>接口自动化测试报告</reportHeadline>
<!-- global date format override -->
<!-- defaults to yyyy-MM-dd -->
<dateFormat>yyyy-MM-dd</dateFormat>
<!-- global time format override -->
<!-- defaults to HH:mm:ss -->
<timeFormat>HH:mm:ss</timeFormat>
<!-- custom javascript -->
<scripts>
<![CDATA[
$(document).ready(function() {
});
]]>
</scripts>
<!-- custom styles -->
<styles>
<![CDATA[
]]>
</styles>
</configuration>
</extentreports>
注意在APICollection-TestSuite.xml中的parameter参数,使用时需传入
十二、配置系统信息
在src/main/java/reporter/config目录下创建MySystemInfo.java类,继承SystemInfo接口
public class MySystemInfo implements SystemInfo {
@Override
public Map<String, String> getSystemInfo() {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
Properties properties = new Properties();
Map<String, String> systemInfo = new HashMap<>();
try {
properties.load(inputStream);
systemInfo.put("environment", properties.getProperty("Environment"));
systemInfo.put("测试人员", "jxq");
} catch (IOException e) {
e.printStackTrace();
}
return systemInfo;
}
}
注意在APICollection-TestSuite.xml中的parameter参数,使用时需传入
集成retrofit2.0
十三、在common下创建HttpBase
HttpBase类中提供了Retrofit基础,考虑到了日常控制台和测试报告上都需要看到对应请求信息,故此在HttpClient中默认加入了日志拦截器;日志拦截器的实现方法里,用Reportes.log记录到日志中。 并且,考虑到实际项目中每个Http请求都会有对应类似RequestHeader、RequestBody的加密签名等,预留了拦截器。 可在HttpBase构造方法时传入对应拦截器。 对应的拦截器可以通过实现接口Interceptor,做对应项目需求操作。
public class HttpBase {
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private Retrofit retrofit;
private String host;
/**
* 构造方法(1个参数)
* 只传Host
* 可以加入自定义拦截器 -超时判断
* 或者,默认没有使用拦截器。
*
* @param host 访问域名host
*/
public HttpBase(String host) {
// 可添加响应超时拦截器
Interceptor interceptor = new MyInterceptor();
init(host, interceptor);
// init(host, null);
}
/**
* 构造方法(2个参数)
* 只传Host,默认使用日志拦截器。
*
* @param host 访问域名host
* @param interceptor 自定义拦截器
*/
public HttpBase(String host, Interceptor interceptor) {
init(host, interceptor);
}
/**
* 初始化方法
*
* @param host 访问域名host
* @param interceptor 自定义拦截器
*/
private void init(String host, Interceptor interceptor) {
OkHttpClient.Builder client = getHttpClient(interceptor);
retrofit = new Retrofit.Builder()
.baseUrl(host)
.client(client.build())
.addConverterFactory(RespVoConverterFactory.create())
.build();
}
/**
* 获取HttpClient.Builder 方法。
* 默认添加了,基础日志拦截器
*
* @param interceptor 拦截器
* @return HttpClient.Builder对象
*/
private OkHttpClient.Builder getHttpClient(Interceptor interceptor) {
HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true);
if (interceptor != null) {
builder.addInterceptor(interceptor);
}
builder.addInterceptor(logging);
return builder;
}
/**
* 日志拦截器
*
* @return
*/
private HttpLoggingInterceptor getHttpLoggingInterceptor() {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Reporter.log("RetrofitLog--> " + message, true);
}
});
logging.setLevel(HttpLoggingInterceptor.Level.BODY);//Level中还有其他等级
return logging;
}
/**
* retrofit构建方法
*
* @param clazz 泛型类
* @param <T> 泛型类
* @return 泛型类
*/
public <T> T create(Class<T> clazz) {
return retrofit.create(clazz);
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
}
同级目录再创建MyInterceptor.java
public class MyInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
if (time > 100) {
MyReporter.report.log(Status.WARNING, MyReporter.getTestName() + " 接口耗时:" + time);
}
return response;
}
}
创建接口ISearch
public interface ISearch {
@GET("j/search_tags")
Call<MovieResponseVO> searchTags(@Query("type") String type, @Query("source") String source);
}
集成HttpBase的Http Api接口请求方法类
在Retrofit已经可以用4行的代码实现Http请求了
HttpBase httpBase = new HttpBase(host);
ISearch iSearch = httpBase.create(ISearch.class);
Call<MovieResponseVO> call = iSearch.searchTags(type, source);
Response<MovieResponseVO> response = call.execute();
上面的4行代码,每次都需要写也是挺麻烦的,所以抽出来,让编写测试用例验证更简洁点
public class HttpSearch extends HttpBase {
private ISearch iSearch;
public HttpSearch(String host) {
super(host);
iSearch = super.create(ISearch.class);
}
public Response<MovieResponseVO> searchTags(String type, String source) throws IOException {
Call<MovieResponseVO> call = iSearch.searchTags(type, source);
return call.execute();
}
// 同模块下,新增的接口可添加到这里。
// public Response<MovieResponseVO> searchTags(String type, String source) throws IOException {
// Call<MovieResponseVO> call = iSearch.searchTags(type, source);
// return call.execute();
// }
}
JsonSchema验证基础响应体(响应为JSON格式可用)
pom.xml 依赖引入
<!--json schema start-->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<dependency>
<groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.6</version>
</dependency>
<!--json schema end-->
简单抽象JsonSchemaUtils工具类
/**
* JsonSchema工具类
*/
public class JsonSchemaUtils {
/**
* 从指定路径读取Schema信息
*
* @param filePath Schema路径
* @return JsonNode型Schema
* @throws IOException 抛出IO异常
*/
private static JsonNode readJSONfile(String filePath) throws IOException {
InputStream stream = JsonSchemaUtils.class.getClassLoader().getResourceAsStream(filePath);
return new JsonNodeReader().fromInputStream(stream);
}
/**
* 将Json的String型转JsonNode类型
*
* @param str 需要转换的Json String对象
* @return 转换JsonNode对象
* @throws IOException 抛出IO异常
*/
private static JsonNode readJSONStr(String str) throws IOException {
return new ObjectMapper().readTree(str);
}
/**
* 将需要验证的JsonNode 与 JsonSchema标准对象 进行比较
*
* @param schema schema标准对象
* @param data 需要比对的Schema对象
*/
private static void assertJsonSchema(JsonNode schema, JsonNode data) {
ProcessingReport report = JsonSchemaFactory.byDefault().getValidator().validateUnchecked(schema, data);
if (!report.isSuccess()) {
for (ProcessingMessage aReport : report) {
Reporter.log(aReport.getMessage(), true);
}
}
Assert.assertTrue(report.isSuccess());
}
/**
* 将需要验证的response 与 JsonSchema标准对象 进行比较
*
* @param schemaPath JsonSchema标准的路径
* @param response 需要验证的response
* @throws IOException 抛出IO异常
*/
public static void assertResponseJsonSchema(String schemaPath, String response) throws IOException {
JsonNode jsonSchema = readJSONfile(schemaPath);
JsonNode responseJN = readJSONStr(response);
assertJsonSchema(jsonSchema, responseJN);
}
}
这里已经将最后抽成简单方法供使用,只需传入schemaPath路劲、以及需要验证的对象。
Http响应体保存到本地
- ①、可以通过客户端抓包获取得到Http响应体、或者开发接口定义文档 等方式,得到最后Http响应体的Json对象。(注意:响应体内容尽量全面,这样在验证时也可以尽可能验证)
- ②、将请求响应体通过 https://jsonschema.net/ ,在线验证得到JsonSchema信息。
③、根据接口响应需求,做基础验证配置。例如,这里将tags字段认为是必须存在的参数。 完整Schema约束文件如下,并将此文件保存到resources目录对应模块下。
{
"$id": "http://example.com/example.json",
"type": "object",
"properties": {
"tags": {
"$id": "/properties/tags",
"type": "array",
"items": {
"$id": "/properties/tags/items",
"type": "string",
"title": "The 0th Schema ",
"default": "",
"examples": [
"热门",
"最新"
]
}
}
},
"required": [
"tags"
]
}
TestCase测试用例编写
public class SearchTagsTest {
private static Properties properties;
private static HttpSearch implSearch;
private static String SCHEMA_PATH = "parameters/search/schema/SearchTagsMovie.json";
@BeforeSuite
public void beforeSuite() throws IOException {
InputStream stream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
properties = new Properties();
properties.load(stream);
String host = properties.getProperty("douban.host");
implSearch = new HttpSearch(host);
stream = this.getClass().getClassLoader().getResourceAsStream("parameters/search/SearchTagsParams.properties");
properties.load(stream);
stream = this.getClass().getClassLoader().getResourceAsStream("");
stream.close();
}
@Test
public void testcase1() throws IOException {
String type = properties.getProperty("testcase1.req.type");
String source = properties.getProperty("testcase1.req.source");
Response<MovieResponseVO> response = implSearch.searchTags(type, source);
MovieResponseVO body = response.body();
Assert.assertNotNull(body, "response.body()");
// 响应返回内容想通过schema标准校验
JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
// 再Json化成对象
Assert.assertNotNull(body.getTags(), "tags");
}
@Test
public void testcase2() throws IOException {
String type = properties.getProperty("testcase2.req.type");
String source = properties.getProperty("testcase2.req.source");
Response<MovieResponseVO> response = implSearch.searchTags(type, source);
MovieResponseVO body = response.body();
Assert.assertNotNull(body, "response.body()");
JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
Assert.assertNotNull(body.getTags(), "tags");
}
}
至此,TestNg测试用例部分全部完成。