创建项目引入依赖
一、创建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文件,该文件记录的信息是跟环境切换相关的参数。
#EnvironmentEnvironment=${Environment}douban.host=${douban.host}
六、在src/main/filters/下创建filter-debug.properties、filter-dev.properties、filter-product.properties用户记录环境信息参数,模板如下
#EnvironmentEnvironment=Debugdouban.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.GITHUBhtmlReporter.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.htmlFile reportFile = new File(reportPath, "report.html");// 邮件报告名emailable-report.htmlFile 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 {@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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 {@Overridepublic 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";@BeforeSuitepublic 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();}@Testpublic 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");}@Testpublic 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测试用例部分全部完成。
