创建项目引入依赖

一、创建maven项目
二、引入依赖

  1. <dependencies>
  2. <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
  3. <dependency>
  4. <groupId>com.alibaba</groupId>
  5. <artifactId>fastjson</artifactId>
  6. <version>1.2.47</version>
  7. </dependency>
  8. <dependency>
  9. <groupId>com.squareup.okhttp3</groupId>
  10. <artifactId>okhttp</artifactId>
  11. <version>3.10.0</version>
  12. </dependency>
  13. <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor 日志拦截器-->
  14. <dependency>
  15. <groupId>com.squareup.okhttp3</groupId>
  16. <artifactId>logging-interceptor</artifactId>
  17. <version>3.10.0</version>
  18. </dependency>
  19. <!-- https://mvnrepository.com/artifact/org.testng/testng -->
  20. <dependency>
  21. <groupId>org.testng</groupId>
  22. <artifactId>testng</artifactId>
  23. <version>6.14.2</version>
  24. <!--<scope>test</scope>-->
  25. <!--作用范围,默认是test。验证部分被抽象,不仅test作用域需使用-->
  26. </dependency>
  27. <!-- https://mvnrepository.com/artifact/com.aventstack/extentreports -->
  28. <dependency>
  29. <groupId>com.aventstack</groupId>
  30. <artifactId>extentreports</artifactId>
  31. <version>3.1.5</version>
  32. <scope>provided</scope>
  33. </dependency>
  34. <!-- https://mvnrepository.com/artifact/com.vimalselvam/testng-extentsreport -->
  35. <dependency>
  36. <groupId>com.vimalselvam</groupId>
  37. <artifactId>testng-extentsreport</artifactId>
  38. <version>1.3.1</version>
  39. </dependency>
  40. <dependency>
  41. <groupId>com.squareup.retrofit2</groupId>
  42. <artifactId>retrofit</artifactId>
  43. <version>2.4.0</version>
  44. </dependency>
  45. <!-- https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson 对象转换器-->
  46. <dependency>
  47. <groupId>com.squareup.retrofit2</groupId>
  48. <artifactId>converter-gson</artifactId>
  49. <version>2.3.0</version>
  50. </dependency>
  51. </dependencies>
  52. <!--为了maven打包编译时后台一直输出警告信息。导致构建失败-->
  53. <properties>
  54. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  55. <xmlFileName></xmlFileName>
  56. </properties>

多环境切换配置

三、配置多环境切换
配置pom.xml文件的 build节点

  1. <build>
  2. <resources>
  3. <resource>
  4. <directory>src/main/resources</directory>
  5. <filtering>true</filtering>
  6. <!--扫描替换参数的文件路径-->
  7. </resource>
  8. </resources>
  9. <filters>
  10. <filter>src/main/filters/filter-${env}.properties</filter>
  11. <!--环境过滤器的配置方式,回头需要在该路径下建立对应文件-->
  12. </filters>
  13. <plugins>
  14. <plugin>
  15. <!--该插件是解决命令下执行mvn test指定testng xxx.xml 文件 的配置-->
  16. <groupId>org.apache.maven.plugins</groupId>
  17. <artifactId>maven-surefire-plugin</artifactId>
  18. <version>2.22.0</version>
  19. <configuration>
  20. <!--为了解决在jenkins maven执行test 报告乱码问题,编码格式设置为UTF-8-->
  21. <argLine>-Dfile.encoding=UTF-8</argLine>
  22. <encoding>UTF-8</encoding>
  23. <!--动态指定执行的xml文件。${project.basedir}项目目录,${xmlFileName}maven文件-->
  24. <suiteXmlFiles>
  25. <suiteXmlFile>${project.basedir}/target/classes/testNg/${xmlFileName}</suiteXmlFile>
  26. <!--在IEDA上运行maven test命令时,可打开该注释使用完整测试集合-->
  27. <!--<suiteXmlFile>${project.basedir}/target/classes/testNg/api/APICollection-TestSuite.xml</suiteXmlFile>-->
  28. </suiteXmlFiles>
  29. </configuration>
  30. </plugin>
  31. <plugin>
  32. <groupId>org.apache.maven.plugins</groupId>
  33. <artifactId>maven-compiler-plugin</artifactId>
  34. <configuration>
  35. <encoding>UTF-8</encoding>
  36. <source>8</source>
  37. <target>8</target>
  38. </configuration>
  39. </plugin>
  40. </plugins>
  41. </build>

四、在pom.xml文件配置Properties环境,多环境配置参数切换。

  1. <profiles>
  2. <!-- 开发环境,默认激活 -->
  3. <profile>
  4. <id>dev</id>
  5. <properties>
  6. <env>dev</env>
  7. </properties>
  8. </profile>
  9. <!-- 生产环境 -->
  10. <profile>
  11. <id>product</id>
  12. <properties>
  13. <env>product</env>
  14. </properties>
  15. </profile>
  16. <!-- 测试环境 -->
  17. <profile>
  18. <id>debug</id>
  19. <properties>
  20. <env>debug</env>
  21. </properties>
  22. <activation>
  23. <activeByDefault>true</activeByDefault><!--默认启用的是dev环境配置-->
  24. </activation>
  25. </profile>
  26. </profiles>

五、在src/resource下创建env.properties文件,该文件记录的信息是跟环境切换相关的参数。

  1. #Environment
  2. Environment=${Environment}
  3. douban.host=${douban.host}

六、在src/main/filters/下创建filter-debug.properties、filter-dev.properties、filter-product.properties用户记录环境信息参数,模板如下

  1. #Environment
  2. Environment=Debug
  3. douban.host=https://movie.douban.com/

配置testNG

七、在src/resources/下创建testNg目录,存放测试集合的目录,为了能够让所有接口统一运行测试,需建立一个所有的测试集合。
APICollection-TestSuite.xml

  1. <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
  2. <suite name="接口测试集合" verbose="1" preserve-order="true">
  3. <!-- 自定义参数,用于报告展示-->
  4. <parameter name="report.config" value="src/main/resources/config/report/extent-config.xml"/>
  5. <parameter name="system.info" value="reporter.config.MySystemInfo"/>
  6. <!-- 执行的测试用例文件-->
  7. <suite-files>
  8. <suite-file path="search/search-TestSuite.xml"/>
  9. </suite-files>
  10. <!-- 报告监听器配置-->
  11. <listeners>
  12. <listener class-name="reporter.listener.MyExtentTestNgFormatter"/>
  13. </listeners>
  14. </suite>

search-TestSuite.xml

  1. <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
  2. <suite name="search_tags-搜索分类-电影首页测试集合" verbose="1" preserve-order="true">
  3. <parameter name="report.config" value="src/main/resources/config/report/extent-config.xml"/>
  4. <parameter name="system.info" value="reporter.config.MySystemInfo"/>
  5. <test name="0100.搜索分类-电影首页-正常场景" preserve-order="true">
  6. <classes>
  7. <class name="com.jxq.douban.SearchTagsTest">
  8. <methods>
  9. <include name="testcase1"/>
  10. </methods>
  11. </class>
  12. </classes>
  13. </test>
  14. <test name="0100.搜索分类-TV首页-正常场景" preserve-order="true">
  15. <classes>
  16. <class name="com.jxq.douban.SearchTagsTest">
  17. <methods>
  18. <include name="testcase2"/>
  19. </methods>
  20. </class>
  21. </classes>
  22. </test>
  23. <listeners>
  24. <listener class-name="reporter.listener.MyExtentTestNgFormatter"/>
  25. </listeners>
  26. </suite>

配置ExtentReports

八、强制重写ExtentTestNgFormatter类
强制重写EExtentTestNgFormatter类主要是以下两点原因:
①、为了能够在报告中展示更多状态类型的测试结果,例如:成功、失败、警告、跳过等测试状态结果。 ②、因为不支持cdn.rawgit.com访问,故替css访问方式。
在src/main/java/reporter/listener路径下创建MyExtentTestNgFormatter.java

  1. package reporter.listener;
  2. import com.aventstack.extentreports.ExtentReports;
  3. import com.aventstack.extentreports.ExtentTest;
  4. import com.aventstack.extentreports.ResourceCDN;
  5. import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
  6. import com.google.common.base.Preconditions;
  7. import com.google.common.base.Strings;
  8. import com.vimalselvam.testng.EmailReporter;
  9. import com.vimalselvam.testng.NodeName;
  10. import com.vimalselvam.testng.SystemInfo;
  11. import com.vimalselvam.testng.listener.ExtentTestNgFormatter;
  12. import org.testng.*;
  13. import org.testng.xml.XmlSuite;
  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.util.ArrayList;
  17. import java.util.List;
  18. import java.util.Map;
  19. public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
  20. private static final String REPORTER_ATTR = "extentTestNgReporter";
  21. private static final String SUITE_ATTR = "extentTestNgSuite";
  22. private ExtentReports reporter;
  23. private List<String> testRunnerOutput;
  24. private Map<String, String> systemInfo;
  25. private ExtentHtmlReporter htmlReporter;
  26. private static ExtentTestNgFormatter instance;
  27. public MyExtentTestNgFormatter() {
  28. setInstance(this);
  29. testRunnerOutput = new ArrayList<>();
  30. String reportPathStr = System.getProperty("reportPath");
  31. File reportPath;
  32. try {
  33. reportPath = new File(reportPathStr);
  34. } catch (NullPointerException e) {
  35. reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
  36. }
  37. if (!reportPath.exists()) {
  38. if (!reportPath.mkdirs()) {
  39. throw new RuntimeException("Failed to create output run directory");
  40. }
  41. }
  42. File reportFile = new File(reportPath, "report.html");
  43. File emailReportFile = new File(reportPath, "emailable-report.html");
  44. htmlReporter = new ExtentHtmlReporter(reportFile);
  45. EmailReporter emailReporter = new EmailReporter(emailReportFile);
  46. reporter = new ExtentReports();
  47. // 如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
  48. htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
  49. reporter.attachReporter(htmlReporter, emailReporter);
  50. }
  51. /**
  52. * Gets the instance of the {@link ExtentTestNgFormatter}
  53. *
  54. * @return The instance of the {@link ExtentTestNgFormatter}
  55. */
  56. public static ExtentTestNgFormatter getInstance() {
  57. return instance;
  58. }
  59. private static void setInstance(ExtentTestNgFormatter formatter) {
  60. instance = formatter;
  61. }
  62. /**
  63. * Gets the system information map
  64. *
  65. * @return The system information map
  66. */
  67. public Map<String, String> getSystemInfo() {
  68. return systemInfo;
  69. }
  70. /**
  71. * Sets the system information
  72. *
  73. * @param systemInfo The system information map
  74. */
  75. public void setSystemInfo(Map<String, String> systemInfo) {
  76. this.systemInfo = systemInfo;
  77. }
  78. public void onStart(ISuite iSuite) {
  79. if (iSuite.getXmlSuite().getTests().size() > 0) {
  80. ExtentTest suite = reporter.createTest(iSuite.getName());
  81. String configFile = iSuite.getParameter("report.config");
  82. if (!Strings.isNullOrEmpty(configFile)) {
  83. htmlReporter.loadXMLConfig(configFile);
  84. }
  85. String systemInfoCustomImplName = iSuite.getParameter("system.info");
  86. if (!Strings.isNullOrEmpty(systemInfoCustomImplName)) {
  87. generateSystemInfo(systemInfoCustomImplName);
  88. }
  89. iSuite.setAttribute(REPORTER_ATTR, reporter);
  90. iSuite.setAttribute(SUITE_ATTR, suite);
  91. }
  92. }
  93. private void generateSystemInfo(String systemInfoCustomImplName) {
  94. try {
  95. Class<?> systemInfoCustomImplClazz = Class.forName(systemInfoCustomImplName);
  96. if (!SystemInfo.class.isAssignableFrom(systemInfoCustomImplClazz)) {
  97. throw new IllegalArgumentException("The given system.info class name <" + systemInfoCustomImplName +
  98. "> should implement the interface <" + SystemInfo.class.getName() + ">");
  99. }
  100. SystemInfo t = (SystemInfo) systemInfoCustomImplClazz.newInstance();
  101. setSystemInfo(t.getSystemInfo());
  102. } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
  103. throw new IllegalStateException(e);
  104. }
  105. }
  106. public void onFinish(ISuite iSuite) {
  107. }
  108. public void onTestStart(ITestResult iTestResult) {
  109. MyReporter.setTestName(iTestResult.getName());
  110. }
  111. public void onTestSuccess(ITestResult iTestResult) {
  112. }
  113. public void onTestFailure(ITestResult iTestResult) {
  114. }
  115. public void onTestSkipped(ITestResult iTestResult) {
  116. }
  117. public void onTestFailedButWithinSuccessPercentage(ITestResult iTestResult) {
  118. }
  119. public void onStart(ITestContext iTestContext) {
  120. ISuite iSuite = iTestContext.getSuite();
  121. ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
  122. ExtentTest testContext = suite.createNode(iTestContext.getName());
  123. // 自定义报告
  124. // 将MyReporter.report静态引用赋值为testContext。
  125. // testContext是@Test每个测试用例时需要的。report.log可以跟随具体的测试用例。另请查阅源码。
  126. MyReporter.report = testContext;
  127. iTestContext.setAttribute("testContext", testContext);
  128. }
  129. public void onFinish(ITestContext iTestContext) {
  130. ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
  131. if (iTestContext.getFailedTests().size() > 0) {
  132. testContext.fail("Failed");
  133. } else if (iTestContext.getSkippedTests().size() > 0) {
  134. testContext.skip("Skipped");
  135. } else {
  136. testContext.pass("Passed");
  137. }
  138. }
  139. public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
  140. if (iInvokedMethod.isTestMethod()) {
  141. ITestContext iTestContext = iTestResult.getTestContext();
  142. ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
  143. ExtentTest test = testContext.createNode(iTestResult.getName(), iInvokedMethod.getTestMethod().getDescription());
  144. iTestResult.setAttribute("test", test);
  145. }
  146. }
  147. public void afterInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
  148. if (iInvokedMethod.isTestMethod()) {
  149. ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
  150. List<String> logs = Reporter.getOutput(iTestResult);
  151. for (String log : logs) {
  152. test.info(log);
  153. }
  154. int status = iTestResult.getStatus();
  155. if (ITestResult.SUCCESS == status) {
  156. test.pass("Passed");
  157. } else if (ITestResult.FAILURE == status) {
  158. test.fail(iTestResult.getThrowable());
  159. } else {
  160. test.skip("Skipped");
  161. }
  162. for (String group : iInvokedMethod.getTestMethod().getGroups()) {
  163. test.assignCategory(group);
  164. }
  165. }
  166. }
  167. /**
  168. * Adds a screen shot image file to the report. This method should be used only in the configuration method
  169. * and the {@link ITestResult} is the mandatory parameter
  170. *
  171. * @param iTestResult The {@link ITestResult} object
  172. * @param filePath The image file path
  173. * @throws IOException {@link IOException}
  174. */
  175. public void addScreenCaptureFromPath(ITestResult iTestResult, String filePath) throws IOException {
  176. ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
  177. test.addScreenCaptureFromPath(filePath);
  178. }
  179. /**
  180. * Adds a screen shot image file to the report. This method should be used only in the
  181. * {@link org.testng.annotations.Test} annotated method
  182. *
  183. * @param filePath The image file path
  184. * @throws IOException {@link IOException}
  185. */
  186. public void addScreenCaptureFromPath(String filePath) throws IOException {
  187. ITestResult iTestResult = Reporter.getCurrentTestResult();
  188. Preconditions.checkState(iTestResult != null);
  189. ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
  190. test.addScreenCaptureFromPath(filePath);
  191. }
  192. /**
  193. * Sets the test runner output
  194. *
  195. * @param message The message to be logged
  196. */
  197. public void setTestRunnerOutput(String message) {
  198. testRunnerOutput.add(message);
  199. }
  200. public void generateReport(List<XmlSuite> list, List<ISuite> list1, String s) {
  201. if (getSystemInfo() != null) {
  202. for (Map.Entry<String, String> entry : getSystemInfo().entrySet()) {
  203. reporter.setSystemInfo(entry.getKey(), entry.getValue());
  204. }
  205. }
  206. reporter.setTestRunnerOutput(testRunnerOutput);
  207. reporter.flush();
  208. }
  209. /**
  210. * Adds the new node to the test. The node name should have been set already using {@link NodeName}
  211. */
  212. public void addNewNodeToTest() {
  213. addNewNodeToTest(NodeName.getNodeName());
  214. }
  215. /**
  216. * Adds the new node to the test with the given node name.
  217. *
  218. * @param nodeName The name of the node to be created
  219. */
  220. public void addNewNodeToTest(String nodeName) {
  221. addNewNode("test", nodeName);
  222. }
  223. /**
  224. * Adds a new node to the suite. The node name should have been set already using {@link NodeName}
  225. */
  226. public void addNewNodeToSuite() {
  227. addNewNodeToSuite(NodeName.getNodeName());
  228. }
  229. /**
  230. * Adds a new node to the suite with the given node name
  231. *
  232. * @param nodeName The name of the node to be created
  233. */
  234. public void addNewNodeToSuite(String nodeName) {
  235. addNewNode(SUITE_ATTR, nodeName);
  236. }
  237. private void addNewNode(String parent, String nodeName) {
  238. ITestResult result = Reporter.getCurrentTestResult();
  239. Preconditions.checkState(result != null);
  240. ExtentTest parentNode = (ExtentTest) result.getAttribute(parent);
  241. ExtentTest childNode = parentNode.createNode(nodeName);
  242. result.setAttribute(nodeName, childNode);
  243. }
  244. /**
  245. * Adds a info log message to the node. The node name should have been set already using {@link NodeName}
  246. *
  247. * @param logMessage The log message string
  248. */
  249. public void addInfoLogToNode(String logMessage) {
  250. addInfoLogToNode(logMessage, NodeName.getNodeName());
  251. }
  252. /**
  253. * Adds a info log message to the node
  254. *
  255. * @param logMessage The log message string
  256. * @param nodeName The name of the node
  257. */
  258. public void addInfoLogToNode(String logMessage, String nodeName) {
  259. ITestResult result = Reporter.getCurrentTestResult();
  260. Preconditions.checkState(result != null);
  261. ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
  262. test.info(logMessage);
  263. }
  264. /**
  265. * Marks the node as failed. The node name should have been set already using {@link NodeName}
  266. *
  267. * @param t The {@link Throwable} object
  268. */
  269. public void failTheNode(Throwable t) {
  270. failTheNode(NodeName.getNodeName(), t);
  271. }
  272. /**
  273. * Marks the given node as failed
  274. *
  275. * @param nodeName The name of the node
  276. * @param t The {@link Throwable} object
  277. */
  278. public void failTheNode(String nodeName, Throwable t) {
  279. ITestResult result = Reporter.getCurrentTestResult();
  280. Preconditions.checkState(result != null);
  281. ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
  282. test.fail(t);
  283. }
  284. /**
  285. * Marks the node as failed. The node name should have been set already using {@link NodeName}
  286. *
  287. * @param logMessage The message to be logged
  288. */
  289. public void failTheNode(String logMessage) {
  290. failTheNode(NodeName.getNodeName(), logMessage);
  291. }
  292. /**
  293. * Marks the given node as failed
  294. *
  295. * @param nodeName The name of the node
  296. * @param logMessage The message to be logged
  297. */
  298. public void failTheNode(String nodeName, String logMessage) {
  299. ITestResult result = Reporter.getCurrentTestResult();
  300. Preconditions.checkState(result != null);
  301. ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
  302. test.fail(logMessage);
  303. }
  304. }

同级目录再创建MyReporter.java,然后注意MyExtentTestNgFormatter.java中使用MyReporter的自定义部分

  1. public class MyReporter {
  2. public static ExtentTest report;
  3. }

导入MyExtentTestNgFormatter监听类

  1. <listeners>
  2. <listener class-name="reporter.Listener.MyExtentTestNgFormatter"/>
  3. </listeners>

九、修改测试报告生成目录(可选),默认生成在test-output/文件夹下,名为report.html、emailable-report.html。

  1. public MyExtentTestNgFormatter() {
  2. setInstance(this);
  3. testRunnerOutput = new ArrayList<>();
  4. // reportPath报告路径
  5. String reportPathStr = System.getProperty("reportPath");
  6. File reportPath;
  7. try {
  8. reportPath = new File(reportPathStr);
  9. } catch (NullPointerException e) {
  10. reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
  11. }
  12. if (!reportPath.exists()) {
  13. if (!reportPath.mkdirs()) {
  14. throw new RuntimeException("Failed to create output run directory");
  15. }
  16. }
  17. // 报告名report.html
  18. File reportFile = new File(reportPath, "report.html");
  19. // 邮件报告名emailable-report.html
  20. File emailReportFile = new File(reportPath, "emailable-report.html");
  21. htmlReporter = new ExtentHtmlReporter(reportFile);
  22. EmailReporter emailReporter = new EmailReporter(emailReportFile);
  23. reporter = new ExtentReports();
  24. reporter.attachReporter(htmlReporter, emailReporter);
  25. }

十、report.log增加状态、耗时记录等

  1. // 根据状态不同添加报告。型如警告
  2. public class MyInterceptor implements Interceptor {
  3. @Override
  4. public Response intercept(Chain chain) throws IOException {
  5. Request request = chain.request();
  6. Response response = chain.proceed(request);
  7. long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
  8. if (time > 100) {
  9. MyReporter.report.log(Status.WARNING, MyReporter.getTestName() + " 接口耗时:" + time);
  10. }
  11. return response;
  12. }
  13. }

十一、配置报告信息
extent reporters支持报告的配置,目前支持的配置内容有title、主题等。
在src/resources/目录下添加 config/report/extent-config.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <extentreports>
  3. <configuration>
  4. <timeStampFormat>yyyy-MM-dd HH:mm:ss</timeStampFormat>
  5. <!-- report theme -->
  6. <!-- standard, dark -->
  7. <theme>dark</theme>
  8. <!-- document encoding -->
  9. <!-- defaults to UTF-8 -->
  10. <encoding>UTF-8</encoding>
  11. <!-- protocol for script and stylesheets -->
  12. <!-- defaults to https -->
  13. <protocol>https</protocol>
  14. <!-- title of the document -->
  15. <documentTitle>QA-接口自动化测试报告</documentTitle>
  16. <!-- report name - displayed at top-nav -->
  17. <reportName>QA-接口自动化测试报告</reportName>
  18. <!-- report headline - displayed at top-nav, after reportHeadline -->
  19. <reportHeadline>接口自动化测试报告</reportHeadline>
  20. <!-- global date format override -->
  21. <!-- defaults to yyyy-MM-dd -->
  22. <dateFormat>yyyy-MM-dd</dateFormat>
  23. <!-- global time format override -->
  24. <!-- defaults to HH:mm:ss -->
  25. <timeFormat>HH:mm:ss</timeFormat>
  26. <!-- custom javascript -->
  27. <scripts>
  28. <![CDATA[
  29. $(document).ready(function() {
  30. });
  31. ]]>
  32. </scripts>
  33. <!-- custom styles -->
  34. <styles>
  35. <![CDATA[
  36. ]]>
  37. </styles>
  38. </configuration>
  39. </extentreports>

注意在APICollection-TestSuite.xml中的parameter参数,使用时需传入
十二、配置系统信息
在src/main/java/reporter/config目录下创建MySystemInfo.java类,继承SystemInfo接口

  1. public class MySystemInfo implements SystemInfo {
  2. @Override
  3. public Map<String, String> getSystemInfo() {
  4. InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
  5. Properties properties = new Properties();
  6. Map<String, String> systemInfo = new HashMap<>();
  7. try {
  8. properties.load(inputStream);
  9. systemInfo.put("environment", properties.getProperty("Environment"));
  10. systemInfo.put("测试人员", "jxq");
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. return systemInfo;
  15. }
  16. }

注意在APICollection-TestSuite.xml中的parameter参数,使用时需传入

集成retrofit2.0

十三、在common下创建HttpBase
HttpBase类中提供了Retrofit基础,考虑到了日常控制台和测试报告上都需要看到对应请求信息,故此在HttpClient中默认加入了日志拦截器;日志拦截器的实现方法里,用Reportes.log记录到日志中。 并且,考虑到实际项目中每个Http请求都会有对应类似RequestHeader、RequestBody的加密签名等,预留了拦截器。 可在HttpBase构造方法时传入对应拦截器。 对应的拦截器可以通过实现接口Interceptor,做对应项目需求操作。

  1. public class HttpBase {
  2. public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
  3. private Retrofit retrofit;
  4. private String host;
  5. /**
  6. * 构造方法(1个参数)
  7. * 只传Host
  8. * 可以加入自定义拦截器 -超时判断
  9. * 或者,默认没有使用拦截器。
  10. *
  11. * @param host 访问域名host
  12. */
  13. public HttpBase(String host) {
  14. // 可添加响应超时拦截器
  15. Interceptor interceptor = new MyInterceptor();
  16. init(host, interceptor);
  17. // init(host, null);
  18. }
  19. /**
  20. * 构造方法(2个参数)
  21. * 只传Host,默认使用日志拦截器。
  22. *
  23. * @param host 访问域名host
  24. * @param interceptor 自定义拦截器
  25. */
  26. public HttpBase(String host, Interceptor interceptor) {
  27. init(host, interceptor);
  28. }
  29. /**
  30. * 初始化方法
  31. *
  32. * @param host 访问域名host
  33. * @param interceptor 自定义拦截器
  34. */
  35. private void init(String host, Interceptor interceptor) {
  36. OkHttpClient.Builder client = getHttpClient(interceptor);
  37. retrofit = new Retrofit.Builder()
  38. .baseUrl(host)
  39. .client(client.build())
  40. .addConverterFactory(RespVoConverterFactory.create())
  41. .build();
  42. }
  43. /**
  44. * 获取HttpClient.Builder 方法。
  45. * 默认添加了,基础日志拦截器
  46. *
  47. * @param interceptor 拦截器
  48. * @return HttpClient.Builder对象
  49. */
  50. private OkHttpClient.Builder getHttpClient(Interceptor interceptor) {
  51. HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
  52. OkHttpClient.Builder builder = new OkHttpClient.Builder()
  53. .connectTimeout(10, TimeUnit.SECONDS)
  54. .retryOnConnectionFailure(true);
  55. if (interceptor != null) {
  56. builder.addInterceptor(interceptor);
  57. }
  58. builder.addInterceptor(logging);
  59. return builder;
  60. }
  61. /**
  62. * 日志拦截器
  63. *
  64. * @return
  65. */
  66. private HttpLoggingInterceptor getHttpLoggingInterceptor() {
  67. HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
  68. @Override
  69. public void log(String message) {
  70. Reporter.log("RetrofitLog--> " + message, true);
  71. }
  72. });
  73. logging.setLevel(HttpLoggingInterceptor.Level.BODY);//Level中还有其他等级
  74. return logging;
  75. }
  76. /**
  77. * retrofit构建方法
  78. *
  79. * @param clazz 泛型类
  80. * @param <T> 泛型类
  81. * @return 泛型类
  82. */
  83. public <T> T create(Class<T> clazz) {
  84. return retrofit.create(clazz);
  85. }
  86. public String getHost() {
  87. return host;
  88. }
  89. public void setHost(String host) {
  90. this.host = host;
  91. }
  92. }

同级目录再创建MyInterceptor.java

  1. public class MyInterceptor implements Interceptor {
  2. @Override
  3. public Response intercept(Chain chain) throws IOException {
  4. Request request = chain.request();
  5. Response response = chain.proceed(request);
  6. long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
  7. if (time > 100) {
  8. MyReporter.report.log(Status.WARNING, MyReporter.getTestName() + " 接口耗时:" + time);
  9. }
  10. return response;
  11. }
  12. }

创建接口ISearch

  1. public interface ISearch {
  2. @GET("j/search_tags")
  3. Call<MovieResponseVO> searchTags(@Query("type") String type, @Query("source") String source);
  4. }

集成HttpBase的Http Api接口请求方法类
在Retrofit已经可以用4行的代码实现Http请求了

  1. HttpBase httpBase = new HttpBase(host);
  2. ISearch iSearch = httpBase.create(ISearch.class);
  3. Call<MovieResponseVO> call = iSearch.searchTags(type, source);
  4. Response<MovieResponseVO> response = call.execute();

上面的4行代码,每次都需要写也是挺麻烦的,所以抽出来,让编写测试用例验证更简洁点

  1. public class HttpSearch extends HttpBase {
  2. private ISearch iSearch;
  3. public HttpSearch(String host) {
  4. super(host);
  5. iSearch = super.create(ISearch.class);
  6. }
  7. public Response<MovieResponseVO> searchTags(String type, String source) throws IOException {
  8. Call<MovieResponseVO> call = iSearch.searchTags(type, source);
  9. return call.execute();
  10. }
  11. // 同模块下,新增的接口可添加到这里。
  12. // public Response<MovieResponseVO> searchTags(String type, String source) throws IOException {
  13. // Call<MovieResponseVO> call = iSearch.searchTags(type, source);
  14. // return call.execute();
  15. // }
  16. }

JsonSchema验证基础响应体(响应为JSON格式可用)

pom.xml 依赖引入

  1. <!--json schema start-->
  2. <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
  3. <dependency>
  4. <groupId>com.fasterxml.jackson.core</groupId>
  5. <artifactId>jackson-core</artifactId>
  6. <version>2.9.6</version>
  7. </dependency>
  8. <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
  9. <dependency>
  10. <groupId>com.fasterxml.jackson.core</groupId>
  11. <artifactId>jackson-databind</artifactId>
  12. <version>2.9.6</version>
  13. </dependency>
  14. <dependency>
  15. <groupId>com.github.fge</groupId>
  16. <artifactId>json-schema-validator</artifactId>
  17. <version>2.2.6</version>
  18. </dependency>
  19. <!--json schema end-->

简单抽象JsonSchemaUtils工具类

  1. /**
  2. * JsonSchema工具类
  3. */
  4. public class JsonSchemaUtils {
  5. /**
  6. * 从指定路径读取Schema信息
  7. *
  8. * @param filePath Schema路径
  9. * @return JsonNode型Schema
  10. * @throws IOException 抛出IO异常
  11. */
  12. private static JsonNode readJSONfile(String filePath) throws IOException {
  13. InputStream stream = JsonSchemaUtils.class.getClassLoader().getResourceAsStream(filePath);
  14. return new JsonNodeReader().fromInputStream(stream);
  15. }
  16. /**
  17. * 将Json的String型转JsonNode类型
  18. *
  19. * @param str 需要转换的Json String对象
  20. * @return 转换JsonNode对象
  21. * @throws IOException 抛出IO异常
  22. */
  23. private static JsonNode readJSONStr(String str) throws IOException {
  24. return new ObjectMapper().readTree(str);
  25. }
  26. /**
  27. * 将需要验证的JsonNode 与 JsonSchema标准对象 进行比较
  28. *
  29. * @param schema schema标准对象
  30. * @param data 需要比对的Schema对象
  31. */
  32. private static void assertJsonSchema(JsonNode schema, JsonNode data) {
  33. ProcessingReport report = JsonSchemaFactory.byDefault().getValidator().validateUnchecked(schema, data);
  34. if (!report.isSuccess()) {
  35. for (ProcessingMessage aReport : report) {
  36. Reporter.log(aReport.getMessage(), true);
  37. }
  38. }
  39. Assert.assertTrue(report.isSuccess());
  40. }
  41. /**
  42. * 将需要验证的response 与 JsonSchema标准对象 进行比较
  43. *
  44. * @param schemaPath JsonSchema标准的路径
  45. * @param response 需要验证的response
  46. * @throws IOException 抛出IO异常
  47. */
  48. public static void assertResponseJsonSchema(String schemaPath, String response) throws IOException {
  49. JsonNode jsonSchema = readJSONfile(schemaPath);
  50. JsonNode responseJN = readJSONStr(response);
  51. assertJsonSchema(jsonSchema, responseJN);
  52. }
  53. }

这里已经将最后抽成简单方法供使用,只需传入schemaPath路劲、以及需要验证的对象。
Http响应体保存到本地

  • ①、可以通过客户端抓包获取得到Http响应体、或者开发接口定义文档 等方式,得到最后Http响应体的Json对象。(注意:响应体内容尽量全面,这样在验证时也可以尽可能验证)
  • ②、将请求响应体通过 https://jsonschema.net/ ,在线验证得到JsonSchema信息。
  • ③、根据接口响应需求,做基础验证配置。例如,这里将tags字段认为是必须存在的参数。 完整Schema约束文件如下,并将此文件保存到resources目录对应模块下。

    1. {
    2. "$id": "http://example.com/example.json",
    3. "type": "object",
    4. "properties": {
    5. "tags": {
    6. "$id": "/properties/tags",
    7. "type": "array",
    8. "items": {
    9. "$id": "/properties/tags/items",
    10. "type": "string",
    11. "title": "The 0th Schema ",
    12. "default": "",
    13. "examples": [
    14. "热门",
    15. "最新"
    16. ]
    17. }
    18. }
    19. },
    20. "required": [
    21. "tags"
    22. ]
    23. }

    TestCase测试用例编写

    1. public class SearchTagsTest {
    2. private static Properties properties;
    3. private static HttpSearch implSearch;
    4. private static String SCHEMA_PATH = "parameters/search/schema/SearchTagsMovie.json";
    5. @BeforeSuite
    6. public void beforeSuite() throws IOException {
    7. InputStream stream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
    8. properties = new Properties();
    9. properties.load(stream);
    10. String host = properties.getProperty("douban.host");
    11. implSearch = new HttpSearch(host);
    12. stream = this.getClass().getClassLoader().getResourceAsStream("parameters/search/SearchTagsParams.properties");
    13. properties.load(stream);
    14. stream = this.getClass().getClassLoader().getResourceAsStream("");
    15. stream.close();
    16. }
    17. @Test
    18. public void testcase1() throws IOException {
    19. String type = properties.getProperty("testcase1.req.type");
    20. String source = properties.getProperty("testcase1.req.source");
    21. Response<MovieResponseVO> response = implSearch.searchTags(type, source);
    22. MovieResponseVO body = response.body();
    23. Assert.assertNotNull(body, "response.body()");
    24. // 响应返回内容想通过schema标准校验
    25. JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
    26. // 再Json化成对象
    27. Assert.assertNotNull(body.getTags(), "tags");
    28. }
    29. @Test
    30. public void testcase2() throws IOException {
    31. String type = properties.getProperty("testcase2.req.type");
    32. String source = properties.getProperty("testcase2.req.source");
    33. Response<MovieResponseVO> response = implSearch.searchTags(type, source);
    34. MovieResponseVO body = response.body();
    35. Assert.assertNotNull(body, "response.body()");
    36. JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
    37. Assert.assertNotNull(body.getTags(), "tags");
    38. }
    39. }

    至此,TestNg测试用例部分全部完成。