├── src
├── main
│ ├── resources
│ │ ├── env.properties
│ │ ├── parameters
│ │ │ └── search
│ │ │ │ ├── SearchTagsParams.properties
│ │ │ │ └── schema
│ │ │ │ └── SearchTagsMovie.json
│ │ ├── testNg
│ │ │ └── api
│ │ │ │ ├── APICollection-TestSuite.xml
│ │ │ │ └── search
│ │ │ │ └── SearchTags-TestSuite.xml
│ │ ├── log4j.properties
│ │ └── config
│ │ │ └── report
│ │ │ └── extent-config.xml
│ ├── filters
│ │ ├── filter-dev.properties
│ │ ├── filter-product.properties
│ │ └── filter-debug.properties
│ ├── webapp
│ │ ├── index.jsp
│ │ └── WEB-INF
│ │ │ └── web.xml
│ └── java
│ │ ├── com
│ │ └── jxq
│ │ │ ├── douban
│ │ │ ├── domain
│ │ │ │ └── MovieResponseVO.java
│ │ │ ├── ISearch.java
│ │ │ └── HttpSearch.java
│ │ │ ├── common
│ │ │ ├── MyInterceptor.java
│ │ │ └── HttpBase.java
│ │ │ └── tools
│ │ │ ├── RespVoConverterFactory.java
│ │ │ └── JsonSchemaUtils.java
│ │ └── reporter
│ │ ├── Listener
│ │ ├── MyReporter.java
│ │ └── MyExtentTestNgFormatter.java
│ │ └── config
│ │ └── MySystemInfo.java
└── test
│ └── java
│ └── com
│ └── jxq
│ └── douban
│ └── SearchTagsTest.java
├── LICENSE
├── .gitignore
├── pom.xml
└── README.md
/src/main/resources/env.properties:
--------------------------------------------------------------------------------
1 | #Environment
2 | Environment=${Environment}
3 |
4 |
5 | douban.host=${douban.host}
--------------------------------------------------------------------------------
/src/main/filters/filter-dev.properties:
--------------------------------------------------------------------------------
1 | #Environment
2 |
3 | Environment=Dev
4 |
5 | douban.host=https://movie.douban.com/
--------------------------------------------------------------------------------
/src/main/filters/filter-product.properties:
--------------------------------------------------------------------------------
1 | #Environment
2 |
3 | Environment=product
4 |
5 | douban.host=https://movie.douban.com/
--------------------------------------------------------------------------------
/src/main/filters/filter-debug.properties:
--------------------------------------------------------------------------------
1 | #Environment
2 |
3 | Environment=Debug
4 |
5 | douban.host=https://movie.douban.com/
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main/resources/parameters/search/SearchTagsParams.properties:
--------------------------------------------------------------------------------
1 | testcase1.req.type=movie
2 | testcase1.req.source=index
3 |
4 | testcase2.req.type=tv
5 | testcase2.req.source=index
6 |
7 |
--------------------------------------------------------------------------------
/src/main/webapp/index.jsp:
--------------------------------------------------------------------------------
1 | <%@ page language="java" pageEncoding="UTF-8" %>
2 |
3 |
4 |
5 |
6 |
7 | Hello World!
8 |
9 |
10 |
11 | Hello World!
12 |
13 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/douban/domain/MovieResponseVO.java:
--------------------------------------------------------------------------------
1 | package com.jxq.douban.domain;
2 |
3 | import java.util.List;
4 |
5 | /**
6 | * @Auther: jx
7 | * @Date: 2018/7/13 18:01
8 | * @Description:
9 | */
10 | public class MovieResponseVO {
11 |
12 | private List tags;
13 |
14 | public List getTags() {
15 | return tags;
16 | }
17 |
18 | public void setTags(List tags) {
19 | this.tags = tags;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/douban/ISearch.java:
--------------------------------------------------------------------------------
1 | package com.jxq.douban;
2 |
3 | import com.jxq.douban.domain.MovieResponseVO;
4 | import retrofit2.Call;
5 | import retrofit2.http.GET;
6 | import retrofit2.http.Query;
7 |
8 | /**
9 | * @Auther: jx
10 | * @Date: 2018/7/13 17:44
11 | * @Description: 豆瓣查询电影分类接口
12 | */
13 | public interface ISearch {
14 | @GET("j/search_tags")
15 | Call searchTags(@Query("type") String type, @Query("source") String source);
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/reporter/Listener/MyReporter.java:
--------------------------------------------------------------------------------
1 | package reporter.Listener;
2 |
3 | import com.aventstack.extentreports.ExtentTest;
4 |
5 | /**
6 | * @Auther: jx
7 | * @Date: 2018/6/27 17:18
8 | * @Description:
9 | */
10 | public class MyReporter {
11 | public static ExtentTest report;
12 | private static String testName;
13 |
14 | public static String getTestName() {
15 | return testName;
16 | }
17 |
18 | public static void setTestName(String testName) {
19 | MyReporter.testName = testName;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/resources/parameters/search/schema/SearchTagsMovie.json:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/src/main/resources/testNg/api/APICollection-TestSuite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | log4j.rootLogger=INFO,Console,File
2 | log4j.appender.Console=org.apache.log4j.ConsoleAppender
3 | log4j.appender.Console.Target=System.out
4 | log4j.appender.Console.layout = org.apache.log4j.PatternLayout
5 | log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n
6 |
7 | log4j.appender.File = org.apache.log4j.RollingFileAppender
8 | log4j.appender.File.File = logs/ssm.log
9 | log4j.appender.File.MaxFileSize = 10MB
10 | log4j.appender.File.Threshold = ALL
11 | log4j.appender.File.layout = org.apache.log4j.PatternLayout
12 | log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n
--------------------------------------------------------------------------------
/src/main/resources/testNg/api/search/SearchTags-TestSuite.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/douban/HttpSearch.java:
--------------------------------------------------------------------------------
1 | package com.jxq.douban;
2 |
3 | import com.jxq.common.HttpBase;
4 | import com.jxq.douban.ISearch;
5 | import com.jxq.douban.domain.MovieResponseVO;
6 | import retrofit2.Call;
7 | import retrofit2.Response;
8 |
9 | import java.io.IOException;
10 |
11 | /**
12 | * @Auther: jx
13 | * @Date: 2018/7/13 17:47
14 | * @Description:
15 | */
16 | public class HttpSearch extends HttpBase {
17 |
18 | private ISearch iSearch;
19 |
20 | public HttpSearch(String host) {
21 | super(host);
22 | iSearch = super.create(ISearch.class);
23 | }
24 |
25 | public Response searchTags(String type, String source) throws IOException {
26 | Call call = iSearch.searchTags(type, source);
27 | return call.execute();
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Simple web application
16 |
17 |
18 | index.jsp
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/common/MyInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.jxq.common;
2 |
3 | import com.aventstack.extentreports.Status;
4 | import okhttp3.Interceptor;
5 | import okhttp3.Request;
6 | import okhttp3.Response;
7 | import reporter.Listener.MyReporter;
8 |
9 | import java.io.IOException;
10 |
11 | /**
12 | * 自定义拦截器--超时拦截器
13 | * 验证响应时间超过100毫秒,则警告。
14 | * 也可以自定义添加拦截器
15 | *
16 | * @author jx
17 | * @Date: 2018/7/20 09:44
18 | */
19 | public class MyInterceptor implements Interceptor {
20 |
21 | @Override
22 | public Response intercept(Chain chain) throws IOException {
23 | Request request = chain.request();
24 | Response response = chain.proceed(request);
25 | long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis();
26 | if (time > 100) {
27 | MyReporter.report.log(Status.WARNING, MyReporter.getTestName() + " 接口耗时:" + time);
28 | }
29 | return response;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/reporter/config/MySystemInfo.java:
--------------------------------------------------------------------------------
1 | package reporter.config;
2 |
3 | import com.vimalselvam.testng.SystemInfo;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.Properties;
10 |
11 | /**
12 | * @Auther: jx
13 | * @Date: 2018/6/7 10:54
14 | * @Description:
15 | */
16 | public class MySystemInfo implements SystemInfo {
17 | @Override
18 | public Map getSystemInfo() {
19 | InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
20 | Properties properties = new Properties();
21 | Map systemInfo = new HashMap<>();
22 | try {
23 | properties.load(inputStream);
24 | systemInfo.put("environment", properties.getProperty("Environment"));
25 | systemInfo.put("测试人员", "jxq");
26 | } catch (IOException e) {
27 | e.printStackTrace();
28 | }
29 | return systemInfo;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 J_先生有点儿屁
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
25 |
26 |
27 | #Built application files
28 | *.apk
29 | *.ap_
30 |
31 | # Files for the Dalvik VM
32 | *.dex
33 |
34 | # Java class files
35 | *.class
36 |
37 | # Generated files
38 | */bin/
39 | */gen/
40 | */out/
41 |
42 | # Gradle files
43 | .gradle/
44 | build/
45 | */build/
46 | gradlew
47 | gradlew.bat
48 |
49 | # Local configuration file (sdk path, etc)
50 | local.properties
51 |
52 | # Proguard folder generated by Eclipse
53 | proguard/
54 |
55 |
56 | # Android Studio Navigation editor temp files
57 | .navigation/
58 |
59 | # Android Studio captures folder
60 | captures/
61 |
62 | # Intellij
63 | *.iml
64 | */*.iml
65 |
66 | # Keystore files
67 | #*.jks
68 | #gradle wrapper
69 | gradle/
70 |
71 | #some local files
72 | */.settings/
73 | */.DS_Store
74 | .DS_Store
75 | */.idea/
76 | .idea/
77 | gradlew
78 | gradlew.bat
79 | unused.txt
80 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/tools/RespVoConverterFactory.java:
--------------------------------------------------------------------------------
1 | package com.jxq.tools;
2 |
3 | import com.alibaba.fastjson.JSONObject;
4 | import com.jxq.douban.domain.MovieResponseVO;
5 | import okhttp3.ResponseBody;
6 | import retrofit2.Converter;
7 | import retrofit2.Retrofit;
8 |
9 | import java.io.IOException;
10 | import java.lang.annotation.Annotation;
11 | import java.lang.reflect.Type;
12 |
13 | /**
14 | * @Auther: jx
15 | * @Date: 2018/7/17 17:29
16 | * @Description:
17 | */
18 | public class RespVoConverterFactory extends Converter.Factory {
19 | public static RespVoConverterFactory create() {
20 | return new RespVoConverterFactory();
21 | }
22 |
23 | private RespVoConverterFactory() {
24 |
25 | }
26 |
27 | @Override
28 | public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
29 | return new RespVoConverter();
30 | }
31 |
32 | public class RespVoConverter implements Converter {
33 | @Override
34 | public MovieResponseVO convert(ResponseBody value) throws IOException {
35 | String temp = value.string();
36 | return JSONObject.parseObject(temp, MovieResponseVO.class);
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/resources/config/report/extent-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | yyyy-MM-dd HH:mm:ss
5 |
6 |
7 | dark
8 |
9 |
10 |
11 | UTF-8
12 |
13 |
14 |
15 | https
16 |
17 |
18 | QA-接口自动化测试报告
19 |
20 |
21 | QA-接口自动化测试报告
22 |
23 |
24 | 接口自动化测试报告
25 |
26 |
27 |
28 | yyyy-MM-dd
29 |
30 |
31 |
32 | HH:mm:ss
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/tools/JsonSchemaUtils.java:
--------------------------------------------------------------------------------
1 | package com.jxq.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.github.fge.jackson.JsonNodeReader;
6 | import com.github.fge.jsonschema.core.report.ProcessingMessage;
7 | import com.github.fge.jsonschema.core.report.ProcessingReport;
8 | import com.github.fge.jsonschema.main.JsonSchemaFactory;
9 | import org.testng.Assert;
10 | import org.testng.Reporter;
11 |
12 | import java.io.IOException;
13 | import java.io.InputStream;
14 |
15 | /**
16 | * JsonSchema工具类
17 | */
18 | public class JsonSchemaUtils {
19 | /**
20 | * 从指定路径读取Schema信息
21 | *
22 | * @param filePath Schema路径
23 | * @return JsonNode型Schema
24 | * @throws IOException 抛出IO异常
25 | */
26 | private static JsonNode readJSONfile(String filePath) throws IOException {
27 | InputStream stream = JsonSchemaUtils.class.getClassLoader().getResourceAsStream(filePath);
28 | return new JsonNodeReader().fromInputStream(stream);
29 | }
30 |
31 |
32 | /**
33 | * 将Json的String型转JsonNode类型
34 | *
35 | * @param str 需要转换的Json String对象
36 | * @return 转换JsonNode对象
37 | * @throws IOException 抛出IO异常
38 | */
39 | private static JsonNode readJSONStr(String str) throws IOException {
40 | return new ObjectMapper().readTree(str);
41 | }
42 |
43 | /**
44 | * 将需要验证的JsonNode 与 JsonSchema标准对象 进行比较
45 | *
46 | * @param schema schema标准对象
47 | * @param data 需要比对的Schema对象
48 | */
49 | private static void assertJsonSchema(JsonNode schema, JsonNode data) {
50 | ProcessingReport report = JsonSchemaFactory.byDefault().getValidator().validateUnchecked(schema, data);
51 | if (!report.isSuccess()) {
52 | for (ProcessingMessage aReport : report) {
53 | Reporter.log(aReport.getMessage(), true);
54 | }
55 | }
56 | Assert.assertTrue(report.isSuccess());
57 | }
58 |
59 |
60 | /**
61 | * 将需要验证的response 与 JsonSchema标准对象 进行比较
62 | *
63 | * @param schemaPath JsonSchema标准的路径
64 | * @param response 需要验证的response
65 | * @throws IOException 抛出IO异常
66 | */
67 | public static void assertResponseJsonSchema(String schemaPath, String response) throws IOException {
68 | JsonNode jsonSchema = readJSONfile(schemaPath);
69 | JsonNode responseJN = readJSONStr(response);
70 | assertJsonSchema(jsonSchema, responseJN);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/test/java/com/jxq/douban/SearchTagsTest.java:
--------------------------------------------------------------------------------
1 | package com.jxq.douban;
2 |
3 | import com.alibaba.fastjson.JSONObject;
4 | import com.jxq.douban.domain.MovieResponseVO;
5 | import com.jxq.tools.JsonSchemaUtils;
6 | import org.testng.Assert;
7 | import org.testng.annotations.BeforeSuite;
8 | import org.testng.annotations.Test;
9 | import retrofit2.Response;
10 |
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.util.Properties;
14 |
15 | /**
16 | * @Auther: jx
17 | * @Date: 2018/7/5 10:48
18 | * @Description: 豆瓣首页接口测试
19 | */
20 | public class SearchTagsTest {
21 | private static Properties properties;
22 | private static HttpSearch implSearch;
23 | private static String SCHEMA_PATH = "parameters/search/schema/SearchTagsMovie.json";
24 |
25 | @BeforeSuite
26 | public void beforeSuite() throws IOException {
27 | InputStream stream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
28 | properties = new Properties();
29 | properties.load(stream);
30 | String host = properties.getProperty("douban.host");
31 | implSearch = new HttpSearch(host);
32 | stream = this.getClass().getClassLoader().getResourceAsStream("parameters/search/SearchTagsParams.properties");
33 | properties.load(stream);
34 | stream = this.getClass().getClassLoader().getResourceAsStream("");
35 | stream.close();
36 | }
37 |
38 | @Test(description = "电影首页。类别:type=movie source=index")
39 | public void testcase1() throws IOException {
40 | String type = properties.getProperty("testcase1.req.type");
41 | String source = properties.getProperty("testcase1.req.source");
42 |
43 | Response response = implSearch.searchTags(type, source);
44 | MovieResponseVO body = response.body();
45 | Assert.assertNotNull(body, "response.body()");
46 | // 响应返回内容想通过schema标准校验
47 | JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
48 | // 再Json化成对象
49 | Assert.assertNotNull(body.getTags(), "tags");
50 | }
51 |
52 | @Test(description = "Tv首页。类别:type=tv source=index")
53 | public void testcase2() throws IOException {
54 | String type = properties.getProperty("testcase2.req.type");
55 | String source = properties.getProperty("testcase2.req.source");
56 | Response response = implSearch.searchTags(type, source);
57 | MovieResponseVO body = response.body();
58 | Assert.assertNotNull(body, "response.body()");
59 | JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
60 | Assert.assertNotNull(body.getTags(), "tags");
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/jxq/common/HttpBase.java:
--------------------------------------------------------------------------------
1 | package com.jxq.common;
2 |
3 | import com.jxq.tools.RespVoConverterFactory;
4 | import okhttp3.Interceptor;
5 | import okhttp3.MediaType;
6 | import okhttp3.OkHttpClient;
7 | import okhttp3.logging.HttpLoggingInterceptor;
8 | import org.testng.Reporter;
9 | import retrofit2.Retrofit;
10 |
11 | import java.util.concurrent.TimeUnit;
12 |
13 | /**
14 | * @Auther: jx
15 | * @Date: 2018/7/13 17:47
16 | * @Description: Http基础使用类。
17 | */
18 | public class HttpBase {
19 | public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
20 | private Retrofit retrofit;
21 | private String host;
22 |
23 | /**
24 | * 构造方法(1个参数)
25 | * 只传Host
26 | * 可以加入自定义拦截器 -超时判断
27 | * 或者,默认没有使用拦截器。
28 | *
29 | * @param host 访问域名host
30 | */
31 | public HttpBase(String host) {
32 | // 可添加响应超时拦截器
33 | Interceptor interceptor = new MyInterceptor();
34 | init(host, interceptor);
35 | // init(host, null);
36 | }
37 |
38 | /**
39 | * 构造方法(2个参数)
40 | * 只传Host,默认使用日志拦截器。
41 | *
42 | * @param host 访问域名host
43 | * @param interceptor 自定义拦截器
44 | */
45 | public HttpBase(String host, Interceptor interceptor) {
46 | init(host, interceptor);
47 | }
48 |
49 | /**
50 | * 初始化方法
51 | *
52 | * @param host 访问域名host
53 | * @param interceptor 自定义拦截器
54 | */
55 | private void init(String host, Interceptor interceptor) {
56 | OkHttpClient.Builder client = getHttpClient(interceptor);
57 | retrofit = new Retrofit.Builder()
58 | .baseUrl(host)
59 | .client(client.build())
60 | .addConverterFactory(RespVoConverterFactory.create())
61 | .build();
62 | }
63 |
64 | /**
65 | * 获取HttpClient.Builder 方法。
66 | * 默认添加了,基础日志拦截器
67 | *
68 | * @param interceptor 拦截器
69 | * @return HttpClient.Builder对象
70 | */
71 | private OkHttpClient.Builder getHttpClient(Interceptor interceptor) {
72 | HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
73 | OkHttpClient.Builder builder = new OkHttpClient.Builder()
74 | .connectTimeout(10, TimeUnit.SECONDS)
75 | .retryOnConnectionFailure(true);
76 | if (interceptor != null) {
77 | builder.addInterceptor(interceptor);
78 | }
79 | builder.addInterceptor(logging);
80 | return builder;
81 | }
82 |
83 | /**
84 | * 日志拦截器
85 | *
86 | * @return
87 | */
88 | private HttpLoggingInterceptor getHttpLoggingInterceptor() {
89 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
90 | @Override
91 | public void log(String message) {
92 | Reporter.log("RetrofitLog--> " + message, true);
93 | }
94 | });
95 | logging.setLevel(HttpLoggingInterceptor.Level.BODY);//Level中还有其他等级
96 | return logging;
97 | }
98 |
99 | /**
100 | * retrofit构建方法
101 | *
102 | * @param clazz 泛型类
103 | * @param 泛型类
104 | * @return 泛型类
105 | */
106 | public T create(Class clazz) {
107 | return retrofit.create(clazz);
108 | }
109 |
110 | public String getHost() {
111 | return host;
112 | }
113 |
114 | public void setHost(String host) {
115 | this.host = host;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | TestHub
8 | TestHub
9 | 1.0-SNAPSHOT
10 | war
11 |
12 |
13 | UTF-8
14 |
15 |
16 |
17 |
18 |
19 |
20 | com.alibaba
21 | fastjson
22 | 1.2.47
23 |
24 |
25 |
26 |
27 | com.googlecode.protobuf-java-format
28 | protobuf-java-format
29 | 1.4
30 |
31 |
32 |
33 |
34 | org.slf4j
35 | slf4j-log4j12
36 | 1.7.25
37 | test
38 |
39 |
40 |
41 | com.squareup.okhttp3
42 | okhttp
43 | 3.10.0
44 |
45 |
46 |
47 | com.squareup.okhttp3
48 | logging-interceptor
49 | 3.10.0
50 |
51 |
52 |
53 |
54 | org.testng
55 | testng
56 | 6.14.2
57 |
58 |
59 |
60 |
61 |
62 | com.aventstack
63 | extentreports
64 | 3.1.5
65 | provided
66 |
67 |
68 |
69 |
70 | com.vimalselvam
71 | testng-extentsreport
72 | 1.3.1
73 |
74 |
75 |
76 | com.squareup.retrofit2
77 | retrofit
78 | 2.4.0
79 |
80 |
81 |
82 | com.squareup.retrofit2
83 | converter-gson
84 | 2.3.0
85 |
86 |
87 |
88 |
89 | log4j
90 | log4j
91 | 1.2.17
92 |
93 |
94 |
95 |
96 |
97 |
98 | com.fasterxml.jackson.core
99 | jackson-core
100 | 2.9.6
101 |
102 |
103 |
104 |
105 |
106 | com.fasterxml.jackson.core
107 | jackson-databind
108 | 2.9.6
109 |
110 |
111 |
112 |
113 | com.github.fge
114 | json-schema-validator
115 | 2.2.6
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | src/main/resources
125 | true
126 |
127 |
128 |
129 |
130 | src/main/filters/filter-${env}.properties
131 |
132 |
133 |
134 | org.apache.maven.plugins
135 | maven-surefire-plugin
136 | 2.22.0
137 |
138 | -Dfile.encoding=UTF-8
139 | UTF-8
140 |
141 | ${project.basedir}/target/classes/testNg/${xmlFileName}
142 |
143 |
144 |
145 |
146 |
147 | org.apache.maven.plugins
148 | maven-compiler-plugin
149 |
150 | UTF-8
151 | 8
152 | 8
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | dev
163 |
164 | dev
165 |
166 |
167 |
168 |
169 |
170 |
171 | product
172 |
173 | product
174 |
175 |
176 |
177 |
178 |
179 |
180 | debug
181 |
182 | debug
183 |
184 |
185 | true
186 |
187 |
188 |
189 |
190 |
--------------------------------------------------------------------------------
/src/main/java/reporter/Listener/MyExtentTestNgFormatter.java:
--------------------------------------------------------------------------------
1 | package reporter.Listener;
2 |
3 | import com.aventstack.extentreports.ExtentReports;
4 | import com.aventstack.extentreports.ExtentTest;
5 | import com.aventstack.extentreports.ResourceCDN;
6 | import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
7 | import com.google.common.base.Preconditions;
8 | import com.google.common.base.Strings;
9 | import com.vimalselvam.testng.EmailReporter;
10 | import com.vimalselvam.testng.NodeName;
11 | import com.vimalselvam.testng.SystemInfo;
12 | import com.vimalselvam.testng.listener.ExtentTestNgFormatter;
13 | import org.testng.*;
14 | import org.testng.xml.XmlSuite;
15 |
16 | import java.io.File;
17 | import java.io.IOException;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
23 | private static final String REPORTER_ATTR = "extentTestNgReporter";
24 | private static final String SUITE_ATTR = "extentTestNgSuite";
25 | private ExtentReports reporter;
26 | private List testRunnerOutput;
27 | private Map systemInfo;
28 | private ExtentHtmlReporter htmlReporter;
29 |
30 | private static ExtentTestNgFormatter instance;
31 |
32 | public MyExtentTestNgFormatter() {
33 | setInstance(this);
34 | testRunnerOutput = new ArrayList<>();
35 | String reportPathStr = System.getProperty("reportPath");
36 | File reportPath;
37 |
38 | try {
39 | reportPath = new File(reportPathStr);
40 | } catch (NullPointerException e) {
41 | reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
42 | }
43 |
44 | if (!reportPath.exists()) {
45 | if (!reportPath.mkdirs()) {
46 | throw new RuntimeException("Failed to create output run directory");
47 | }
48 | }
49 |
50 | File reportFile = new File(reportPath, "report.html");
51 | File emailReportFile = new File(reportPath, "emailable-report.html");
52 |
53 | htmlReporter = new ExtentHtmlReporter(reportFile);
54 | EmailReporter emailReporter = new EmailReporter(emailReportFile);
55 | reporter = new ExtentReports();
56 | // 如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
57 | htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
58 | reporter.attachReporter(htmlReporter, emailReporter);
59 | }
60 |
61 | /**
62 | * Gets the instance of the {@link ExtentTestNgFormatter}
63 | *
64 | * @return The instance of the {@link ExtentTestNgFormatter}
65 | */
66 | public static ExtentTestNgFormatter getInstance() {
67 | return instance;
68 | }
69 |
70 | private static void setInstance(ExtentTestNgFormatter formatter) {
71 | instance = formatter;
72 | }
73 |
74 | /**
75 | * Gets the system information map
76 | *
77 | * @return The system information map
78 | */
79 | public Map getSystemInfo() {
80 | return systemInfo;
81 | }
82 |
83 | /**
84 | * Sets the system information
85 | *
86 | * @param systemInfo The system information map
87 | */
88 | public void setSystemInfo(Map systemInfo) {
89 | this.systemInfo = systemInfo;
90 | }
91 |
92 | public void onStart(ISuite iSuite) {
93 | if (iSuite.getXmlSuite().getTests().size() > 0) {
94 | ExtentTest suite = reporter.createTest(iSuite.getName());
95 | String configFile = iSuite.getParameter("report.config");
96 |
97 | if (!Strings.isNullOrEmpty(configFile)) {
98 | htmlReporter.loadXMLConfig(configFile);
99 | }
100 |
101 | String systemInfoCustomImplName = iSuite.getParameter("system.info");
102 | if (!Strings.isNullOrEmpty(systemInfoCustomImplName)) {
103 | generateSystemInfo(systemInfoCustomImplName);
104 | }
105 |
106 | iSuite.setAttribute(REPORTER_ATTR, reporter);
107 | iSuite.setAttribute(SUITE_ATTR, suite);
108 | }
109 | }
110 |
111 | private void generateSystemInfo(String systemInfoCustomImplName) {
112 | try {
113 | Class> systemInfoCustomImplClazz = Class.forName(systemInfoCustomImplName);
114 | if (!SystemInfo.class.isAssignableFrom(systemInfoCustomImplClazz)) {
115 | throw new IllegalArgumentException("The given system.info class name <" + systemInfoCustomImplName +
116 | "> should implement the interface <" + SystemInfo.class.getName() + ">");
117 | }
118 |
119 | SystemInfo t = (SystemInfo) systemInfoCustomImplClazz.newInstance();
120 | setSystemInfo(t.getSystemInfo());
121 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
122 | throw new IllegalStateException(e);
123 | }
124 | }
125 |
126 | public void onFinish(ISuite iSuite) {
127 | }
128 |
129 |
130 | public void onTestStart(ITestResult iTestResult) {
131 | MyReporter.setTestName(iTestResult.getName());
132 | }
133 |
134 | public void onTestSuccess(ITestResult iTestResult) {
135 |
136 | }
137 |
138 | public void onTestFailure(ITestResult iTestResult) {
139 |
140 | }
141 |
142 | public void onTestSkipped(ITestResult iTestResult) {
143 |
144 | }
145 |
146 | public void onTestFailedButWithinSuccessPercentage(ITestResult iTestResult) {
147 |
148 | }
149 |
150 | public void onStart(ITestContext iTestContext) {
151 | ISuite iSuite = iTestContext.getSuite();
152 | ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
153 | ExtentTest testContext = suite.createNode(iTestContext.getName());
154 | // 自定义报告
155 | MyReporter.report = testContext;
156 | iTestContext.setAttribute("testContext", testContext);
157 | }
158 |
159 | public void onFinish(ITestContext iTestContext) {
160 | ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
161 | if (iTestContext.getFailedTests().size() > 0) {
162 | testContext.fail("Failed");
163 | } else if (iTestContext.getSkippedTests().size() > 0) {
164 | testContext.skip("Skipped");
165 | } else {
166 | testContext.pass("Passed");
167 | }
168 | }
169 |
170 | public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
171 | if (iInvokedMethod.isTestMethod()) {
172 | ITestContext iTestContext = iTestResult.getTestContext();
173 | ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
174 | ExtentTest test = testContext.createNode(iTestResult.getName(), iInvokedMethod.getTestMethod().getDescription());
175 | iTestResult.setAttribute("test", test);
176 | }
177 | }
178 |
179 | public void afterInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
180 | if (iInvokedMethod.isTestMethod()) {
181 | ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
182 | List logs = Reporter.getOutput(iTestResult);
183 | for (String log : logs) {
184 | test.info(log);
185 | }
186 |
187 | int status = iTestResult.getStatus();
188 | if (ITestResult.SUCCESS == status) {
189 | test.pass("Passed");
190 | } else if (ITestResult.FAILURE == status) {
191 | test.fail(iTestResult.getThrowable());
192 | } else {
193 | test.skip("Skipped");
194 | }
195 |
196 | for (String group : iInvokedMethod.getTestMethod().getGroups()) {
197 | test.assignCategory(group);
198 | }
199 | }
200 | }
201 |
202 | /**
203 | * Adds a screen shot image file to the report. This method should be used only in the configuration method
204 | * and the {@link ITestResult} is the mandatory parameter
205 | *
206 | * @param iTestResult The {@link ITestResult} object
207 | * @param filePath The image file path
208 | * @throws IOException {@link IOException}
209 | */
210 | public void addScreenCaptureFromPath(ITestResult iTestResult, String filePath) throws IOException {
211 | ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
212 | test.addScreenCaptureFromPath(filePath);
213 | }
214 |
215 | /**
216 | * Adds a screen shot image file to the report. This method should be used only in the
217 | * {@link org.testng.annotations.Test} annotated method
218 | *
219 | * @param filePath The image file path
220 | * @throws IOException {@link IOException}
221 | */
222 | public void addScreenCaptureFromPath(String filePath) throws IOException {
223 | ITestResult iTestResult = Reporter.getCurrentTestResult();
224 | Preconditions.checkState(iTestResult != null);
225 | ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
226 | test.addScreenCaptureFromPath(filePath);
227 | }
228 |
229 | /**
230 | * Sets the test runner output
231 | *
232 | * @param message The message to be logged
233 | */
234 | public void setTestRunnerOutput(String message) {
235 | testRunnerOutput.add(message);
236 | }
237 |
238 | public void generateReport(List list, List list1, String s) {
239 | if (getSystemInfo() != null) {
240 | for (Map.Entry entry : getSystemInfo().entrySet()) {
241 | reporter.setSystemInfo(entry.getKey(), entry.getValue());
242 | }
243 | }
244 | reporter.setTestRunnerOutput(testRunnerOutput);
245 | reporter.flush();
246 | }
247 |
248 | /**
249 | * Adds the new node to the test. The node name should have been set already using {@link NodeName}
250 | */
251 | public void addNewNodeToTest() {
252 | addNewNodeToTest(NodeName.getNodeName());
253 | }
254 |
255 | /**
256 | * Adds the new node to the test with the given node name.
257 | *
258 | * @param nodeName The name of the node to be created
259 | */
260 | public void addNewNodeToTest(String nodeName) {
261 | addNewNode("test", nodeName);
262 | }
263 |
264 | /**
265 | * Adds a new node to the suite. The node name should have been set already using {@link NodeName}
266 | */
267 | public void addNewNodeToSuite() {
268 | addNewNodeToSuite(NodeName.getNodeName());
269 | }
270 |
271 | /**
272 | * Adds a new node to the suite with the given node name
273 | *
274 | * @param nodeName The name of the node to be created
275 | */
276 | public void addNewNodeToSuite(String nodeName) {
277 | addNewNode(SUITE_ATTR, nodeName);
278 | }
279 |
280 | private void addNewNode(String parent, String nodeName) {
281 | ITestResult result = Reporter.getCurrentTestResult();
282 | Preconditions.checkState(result != null);
283 | ExtentTest parentNode = (ExtentTest) result.getAttribute(parent);
284 | ExtentTest childNode = parentNode.createNode(nodeName);
285 | result.setAttribute(nodeName, childNode);
286 | }
287 |
288 | /**
289 | * Adds a info log message to the node. The node name should have been set already using {@link NodeName}
290 | *
291 | * @param logMessage The log message string
292 | */
293 | public void addInfoLogToNode(String logMessage) {
294 | addInfoLogToNode(logMessage, NodeName.getNodeName());
295 | }
296 |
297 | /**
298 | * Adds a info log message to the node
299 | *
300 | * @param logMessage The log message string
301 | * @param nodeName The name of the node
302 | */
303 | public void addInfoLogToNode(String logMessage, String nodeName) {
304 | ITestResult result = Reporter.getCurrentTestResult();
305 | Preconditions.checkState(result != null);
306 | ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
307 | test.info(logMessage);
308 | }
309 |
310 | /**
311 | * Marks the node as failed. The node name should have been set already using {@link NodeName}
312 | *
313 | * @param t The {@link Throwable} object
314 | */
315 | public void failTheNode(Throwable t) {
316 | failTheNode(NodeName.getNodeName(), t);
317 | }
318 |
319 | /**
320 | * Marks the given node as failed
321 | *
322 | * @param nodeName The name of the node
323 | * @param t The {@link Throwable} object
324 | */
325 | public void failTheNode(String nodeName, Throwable t) {
326 | ITestResult result = Reporter.getCurrentTestResult();
327 | Preconditions.checkState(result != null);
328 | ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
329 | test.fail(t);
330 | }
331 |
332 | /**
333 | * Marks the node as failed. The node name should have been set already using {@link NodeName}
334 | *
335 | * @param logMessage The message to be logged
336 | */
337 | public void failTheNode(String logMessage) {
338 | failTheNode(NodeName.getNodeName(), logMessage);
339 | }
340 |
341 | /**
342 | * Marks the given node as failed
343 | *
344 | * @param nodeName The name of the node
345 | * @param logMessage The message to be logged
346 | */
347 | public void failTheNode(String nodeName, String logMessage) {
348 | ITestResult result = Reporter.getCurrentTestResult();
349 | Preconditions.checkState(result != null);
350 | ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
351 | test.fail(logMessage);
352 | }
353 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TestHub
2 | # 接口自动化测试一体式解决方案
3 | (集成:Java+Testng+Maven+Jenkins+ExtentReports+Retrofit2+Git)
4 |
5 | > 前戏叨逼叨:
6 | 测试多年工作经验,很少有写文章、博客之类的东西。
7 | 其实我这人不爱去写博客之类的东西,更多的是靠脑子的总结。不是脑子好用,其实就一句话:懒!就是懒!!!
8 | 早在几年前就有记录的想法,但当时确实因为工作原因 加上懒,就借口了自己。
9 | 好了,如今就要奔三的人,该面对的往往都要面对。
10 | 于是觉得是该写点东西记录下,倒不是说要总结什么的;只是想让自己经历过的事情,通过记录的形式加强印象;与此同时,希望能够给像自己这样的小白,在遇到类似问题时能够带来想法。
11 | 所以,第一次写类似测试技术博客,献丑。看官海涵~
12 | 第一篇不谈理论,实践篇。
13 | 叨逼叨结束!。
14 |
15 | ### 无聊的背景
16 | 测试工作多年,一路以来一直都会伴随着服务端的测试。
17 | 那么首当其冲的肯定是接口测试, 而接口测试中首先联想到的是接口自动化测试。
18 | 注意:服务端测试 != 接口测试 != 接口自动化测试 。这个公式是不等!找机会再写篇文章详细聊聊...
19 | 在经历过多中不同的平台后,毅然而然的觉得通过码代码的用例是最靠谱的。别相信那些带UI降低编写接口自动化用例难度的自动化平台,原因不详!(不想详细解释,入坑后自会明白...)
20 | 好了,今天介绍一款接口自动化测试的一体式解决方案。(有点夸张,其实吧还真是一体式。)这是入坑后,个人认为在Java大背景服务开发下比较理想的解决方案。
21 | 最后,接口自动化测试的框架和平台形形色色,不用评论哪个好,哪个差。只有最合适项目团队的才是最好的。(废话...)
22 |
23 | 本文代码github:https://github.com/Jsir07/TestHub
24 |
25 | 话不多说,先上Jenkins上自助运行用例,查看报告流程截图。也可结合持续集成自动触发测试服务。
26 | 
27 |
28 | ## 一、方案介绍
29 | ### ①. 选型:Java + Maven + Testng + ExtentReports + Git + Retrofit2 + Jenkins
30 | - 使用Java作为项目编程语言。
31 | - 使用Maven作为项目类型,方便管理架包。
32 | - 使用TestNG作为项目运行框架,方便执行测试用例,生成测试报告。
33 | - 使用ExtentReports作为代替TestNG报告的报告驱动,二次美化功能,界面更美观,内容清晰
34 | - 使用Git作为仓库管理工具,方便管理项目代码。
35 | - 使用Retrofit2作为API接口自动化项目底层服务驱动框架。
36 | - 使用Jenkins作为自动化持续集成平台,方便自动编译,自动打包,自动运行测试脚本,邮件发送测试报告,通知等。
37 |
38 | ### ②. 功能介绍:
39 | 0. 实现持续集成测试,自助式测试,一站式测试平台。
40 | 1. 通过Retrofit2作为等常用接口定义与请求方法,使用方便简洁。同时可分离接口定义、实现请求、响应验证。
41 | 2. 参数化驱动用例运行方式,目前使用本地配置文件;可扩展为造数据。
42 | 3. 还有...自己体会...
43 |
44 | ## 二、环境安装与配置
45 | #### (一)开发环境:
46 | 1. JDK1.7 及以上
47 | 2. IDEA 社区版(壕->pro)
48 | 3. Maven 不限
49 | 4. Git 不限
50 | 5. Jenkins 不限
51 |
52 | #### (二)部分环境安装细节:
53 | ##### 1. JDK 安装请查阅。https://www.cnblogs.com/ottox/p/3313540.html
54 | ##### 2. Maven 安装与配置。
55 | - setting配置,主要是国内镜像 阿里云。https://blog.csdn.net/liuhui_306/article/details/52822152
56 | - 加速。
57 | ```
58 |
59 | alimaven
60 | aliyun maven
61 | http://maven.aliyun.com/nexus/content/groups/public/
62 | central
63 |
64 |
65 | ```
66 | - 本地localRepository配置。为什么要配置这个?因为默认是放在C盘的。```Default: ${user.home}/.m2/repository。```jar 太多、太大!
67 |
68 | ```
69 | E:\apache-maven-3.5.3\repository
70 | ```
71 |
72 | - 命令配置。https://www.cnblogs.com/eagle6688/p/7838224.html
73 | - 为啥配置这个?因为后面Jenkins如果需要在本地运行的话,需要用到mvn命令。
74 | 
75 |
76 |
77 | ##### 3. IDEA 安装请查阅,下载社区版本即可。 https://www.jetbrains.com/idea/
78 | - 编码设置UTF-8:https://blog.csdn.net/frankcheng5143/article/details/50779149
79 | - 字体设置:https://blog.csdn.net/qq_35246620/article/details/63253518
80 | - 模板设置:https://blog.csdn.net/xiaoliulang0324/article/details/79030752
81 | - JDK配置:选择JDK的存放路径。
82 | - maven配置:file->settings->搜索maven->
83 | > Maven home deirectory: maven存放地址。
84 | > User settings file: maven的setting路径。
85 | > Local repository: repository 要存放的路径。
86 |
87 | - 以上配置最好设置成默认配置,这样不用针对每个项目配置。File->Other settings -> Default Settings.
88 |
89 | ##### 4. Git 安装。
90 | 参考:https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git
91 | ##### 5. Jenkins本地安装,只要安装不用创建任务,后面会有任务创建。
92 | 参考:https://www.cnblogs.com/c9999/p/6399367.html。
93 |
94 | *若遇网站需要翻墙,具体下载安装请自行百度。*
95 |
96 | ## 三、框架搭建
97 | ### (一) 项目基础工程
98 | 搭工程,建立基本工程框架,采用maven结构框架工程。 话不多说,先搭建工程。方式:
99 | ##### 1.1 IDEA 上,File ->New ->Project -> maven. 选maven后,不选任何模板,直接Next。
100 | 
101 |
102 | ##### 1.2 填写对应项目信息后,next。
103 | 
104 | ##### 1.3 继续填写 对应信息后,Finish。
105 | 
106 |
107 | ### (二)Maven pom.xml文件配置 与多环境切换
108 | ##### 2.1 依赖配置
109 | 需要使用到的依赖有testng、extentreports、retrofit、fastjson、okhttp等..
110 |
111 |
112 |
113 |
114 | com.alibaba
115 | fastjson
116 | 1.2.47
117 |
118 |
119 |
120 | com.squareup.okhttp3
121 | okhttp
122 | 3.10.0
123 |
124 |
125 |
126 | com.squareup.okhttp3
127 | logging-interceptor
128 | 3.10.0
129 |
130 |
131 |
132 |
133 | org.testng
134 | testng
135 | 6.14.2
136 |
137 |
138 |
139 |
140 |
141 |
142 | com.aventstack
143 | extentreports
144 | 3.1.5
145 | provided
146 |
147 |
148 |
149 |
150 | com.vimalselvam
151 | testng-extentsreport
152 | 1.3.1
153 |
154 |
155 |
156 | com.squareup.retrofit2
157 | retrofit
158 | 2.4.0
159 |
160 |
161 |
162 | com.squareup.retrofit2
163 | converter-gson
164 | 2.3.0
165 |
166 |
167 |
168 |
169 | ##### 2.2 配置多环境切换部分
170 | 采用maven环境切换方式。
171 | - 2.2.1 先配置pom.xml文件的 build节点。
172 |
173 | ```
174 |
175 |
176 |
177 | src/main/resources
178 | true
179 |
180 |
181 |
182 |
183 | src/main/filters/filter-${env}.properties
184 |
185 |
186 |
187 |
188 |
189 | org.apache.maven.plugins
190 | maven-surefire-plugin
191 | 2.22.0
192 |
193 |
194 | -Dfile.encoding=UTF-8
195 | UTF-8
196 |
197 |
198 | ${project.basedir}/target/classes/testNg/${xmlFileName}
199 |
200 |
201 |
202 |
203 | org.apache.maven.plugins
204 | maven-compiler-plugin
205 |
206 | UTF-8
207 | 8
208 | 8
209 |
210 |
211 |
212 |
213 | ```
214 |
215 | - 2.2.2 在pom.xml文件配置properties为了maven打包编译时后台一直输出警告信息。导致构建失败。
216 | ```
217 |
218 |
219 | UTF-8
220 |
221 |
222 | ```
223 |
224 | - 2.2.3 在pom.xml文件配置Properties环境。多环境配置参数切换。
225 | ```
226 |
227 |
228 |
229 | dev
230 |
231 | dev
232 |
233 |
234 |
235 |
236 |
237 | product
238 |
239 | product
240 |
241 |
242 |
243 |
244 |
245 | debug
246 |
247 | debug
248 |
249 |
250 | true
251 |
252 |
253 |
254 | ```
255 |
256 | ##### 2.3 配置环境切换-文件部分
257 | - 2.3.1 在src/resource下创建env.properties文件。
258 | 该文件记录的信息是跟环境切换相关的参数。
259 | 如:接口请求不同环境的host,mongodb、mysql数据库等,因不同环境的信息。
260 | 例如:
261 | ```
262 | api.post.host=${api.post.host}
263 | ```
264 | - 2.3.2 创建src/main/filters/下创建
265 | filter-debug.properties、filter-dev.properties、filter-product.properties
266 | 用于环境信息记录。
267 | 详细信息,如:
268 | ```
269 | api.post.host=http://apidebug.xxx.com:80/
270 | ```
271 | 如图:
272 |
273 | 
274 |
275 | ### (三) testNg配置部分
276 | ##### 3.1. 在src/resources/下创建testNg目录.
277 | src/resources/testNg 目录是存放测试集合的目录,可根据测试模块创建对应模块文件夹。
278 |
279 | ###### 3.1.1 每个文件夹可以是独立模块,每个模块下可以有模块的测试集合。
280 | - 例如,在src/resources/testNg/下创建测试集合:api/search/search-TestSuite.xml。配置如下:
281 | ```
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | ```
313 |
314 | ###### 3.1.2.为了能够让所有接口统一运行测试,需建立一个所有的测试集合,测试集合一般放在src/resources/testNg 目录下。
315 | 型如:
316 | ```
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 | ```
332 |
333 | ###### 3.1.3 说明
334 | - 有同学会问:为什么会有这么多inclde name?
335 | 原因是:test里的name是针对一整个testclass的名称,为了让每一个测试名称都能够展示。
336 | 故此把层级调整了。调整后的层级对应如下:
337 |
338 | | testng层级| 接口映射关系 |
339 | | ------------- |:-------------:|
340 | | test | 接口具体的testcase |
341 | | suite | 具体接口所有用例集合 |
342 | | 总suite | 所有接口测试集合 |
343 |
344 | *需要说明的是,在testng接口层级管理上,每个项目可以有各自的方式,只要符合你的是好的。*
345 |
346 | - 这里添加了parameter、listener,是用于替换testng默认报告,使用ExtentReported。关于ExtentReported后面会有说明。
347 | ```
348 |
349 |
350 |
351 |
352 |
353 |
354 | ```
355 |
356 | ### (四) TestCase测试用例
357 | ##### 4.1 Test用例类
358 | - 首先,满足maven工程结构在src/test目录下建立对应的测试用例类。
359 | - 其次,测试用例类的命名也满足maven在单元测试时的规范。类名以Test结尾.```如:SearchTagsTest.java```
360 | - 最后,根据各模块分类建立对应模块包。
361 |
362 | ##### 4.2 测试用例怎么写?
363 | 其实网上有很多关于TestNg的文章,这边就不做过多介绍了。
364 | > 官网:https://testng.org/doc/index.html
365 | > 也可查阅:https://www.yiibai.com/testng/
366 |
367 | *写接口用例,不在与形式,形式网上很多。
368 | 最重要的是编写的层次与心得。回头有空再整理下。*
369 |
370 | ### (五) extentreports报告
371 | > extentreports是什么?网上有很多关于使用extentreports替代TestNg自带报告。原因是什么?
372 | > 漂亮。先上张图。
373 |
374 | 
375 | 官网很重要:http://extentreports.com/. 其实官网已经给了很多demo了,这里我根据自己的经验进行了配置。
376 |
377 | testNg原有报告有点丑,信息整理有点乱。ExtentReports是用于替换testNg原有的报告。也可以使用ReportNg,个人偏好ExtentReports样式。
378 |
379 | ##### 5.1 强制重写ExtentTestNgFormatter类
380 | 强制重写EExtentTestNgFormatter类主要是以下两点原因:
381 | ①、为了能够在报告中展示更多状态类型的测试结果,例如:成功、失败、警告、跳过等测试状态结果。
382 | ②、因为不支持cdn.rawgit.com访问,故替css访问方式。
383 |
384 | 方式如下:下载ExtentReportes源码,找到ExtentTestNgFormatter类。
385 | - 5.1.1 在创建类:src/main/java/reporter/listener路径下MyExtentTestNgFormatter.java类。
386 | MyExtentTestNgFormatter直接从ExtentTestNgFormatter继承。
387 | ```
388 | public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
389 | ```
390 | - 5.1.2 构造方法加入```htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);```
391 | MyExtentTestNgFormatter 类,代码如下:
392 | ```
393 | public MyExtentTestNgFormatter() {
394 | setInstance(this);
395 | testRunnerOutput = new ArrayList<>();
396 | String reportPathStr = System.getProperty("reportPath");
397 | File reportPath;
398 |
399 | try {
400 | reportPath = new File(reportPathStr);
401 | } catch (NullPointerException e) {
402 | reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
403 | }
404 |
405 | if (!reportPath.exists()) {
406 | if (!reportPath.mkdirs()) {
407 | throw new RuntimeException("Failed to create output run directory");
408 | }
409 | }
410 |
411 | File reportFile = new File(reportPath, "report.html");
412 | File emailReportFile = new File(reportPath, "emailable-report.html");
413 |
414 | htmlReporter = new ExtentHtmlReporter(reportFile);
415 | EmailReporter emailReporter = new EmailReporter(emailReportFile);
416 | reporter = new ExtentReports();
417 | // 如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
418 | htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
419 | reporter.attachReporter(htmlReporter, emailReporter);
420 | }
421 | ```
422 |
423 | - 5.1.3 接着在onstart方法重写功能。
424 | 用了很粗暴的方式,新建了一个类名为MyReporter,一个静态ExtentTest的引用。
425 | - ① reporter.Listener包下MyReporter.java
426 | ```
427 | public class MyReporter {
428 | public static ExtentTest report;
429 | }
430 | ```
431 |
432 | - ② MyExtentTestNgFormatter.java
433 | ```
434 | public void onStart(ITestContext iTestContext) {
435 | ISuite iSuite = iTestContext.getSuite();
436 | ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
437 | ExtentTest testContext = suite.createNode(iTestContext.getName());
438 | // 将MyReporter.report静态引用赋值为testContext。
439 | // testContext是@Test每个测试用例时需要的。report.log可以跟随具体的测试用例。另请查阅源码。
440 | MyReporter.report = testContext;
441 | iTestContext.setAttribute("testContext", testContext);
442 | }
443 | ```
444 |
445 | - 5.1.4 顺带提一句,测试报告默认是在工程根目录下创建test-output/文件夹下,名为report.html、emailable-report.html。可根据各自需求在构造方法中修改。
446 | ```
447 | public MyExtentTestNgFormatter() {
448 | setInstance(this);
449 | testRunnerOutput = new ArrayList<>();
450 | // reportPath报告路径
451 | String reportPathStr = System.getProperty("reportPath");
452 | File reportPath;
453 |
454 | try {
455 | reportPath = new File(reportPathStr);
456 | } catch (NullPointerException e) {
457 | reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
458 | }
459 |
460 | if (!reportPath.exists()) {
461 | if (!reportPath.mkdirs()) {
462 | throw new RuntimeException("Failed to create output run directory");
463 | }
464 | }
465 | // 报告名report.html
466 | File reportFile = new File(reportPath, "report.html");
467 | // 邮件报告名emailable-report.html
468 | File emailReportFile = new File(reportPath, "emailable-report.html");
469 |
470 | htmlReporter = new ExtentHtmlReporter(reportFile);
471 | EmailReporter emailReporter = new EmailReporter(emailReportFile);
472 | reporter = new ExtentReports();
473 | reporter.attachReporter(htmlReporter, emailReporter);
474 | }
475 | ```
476 | - 5.1.5 顺带再提一句,report.log 可以有多种玩法。
477 | ```
478 | // 根据状态不同添加报告。型如警告
479 | MyReporter.report.log(Status.WARNING, "接口耗时(ms):" + String.valueOf(time));
480 | ```
481 | *直接从TestClass中运行时会报MyReporter.report的空指针错误,需做个判空即可。*
482 |
483 | ##### 5.2 导入MyExtentTestNgFormatter监听类
484 | 在测试集合.xml文件中导入Listener监听类。
485 | ```
486 |
487 |
488 |
489 | ```
490 |
491 | ##### 5.3 配置报告信息
492 | extent reporters支持报告的配置。目前支持的配置内容有title、主题等。
493 |
494 | - 先在src/resources/目录下添加 config/report/extent-config.xml。
495 |
496 | - 配置内容
497 | ```
498 |
499 |
500 |
501 | yyyy-MM-dd HH:mm:ss
502 |
503 |
504 | dark
505 |
506 |
507 |
508 | UTF-8
509 |
510 |
511 |
512 | https
513 |
514 |
515 | QA-接口自动化测试报告
516 |
517 |
518 | QA-接口自动化测试报告
519 |
520 |
521 | 接口自动化测试报告
522 |
523 |
524 |
525 | yyyy-MM-dd
526 |
527 |
528 |
529 | HH:mm:ss
530 |
531 |
532 |
533 |
538 |
539 |
540 |
541 |
542 |
545 |
546 |
547 |
548 | ```
549 | ##### 5.4 添加系统信息
550 | 不多说,上图。
551 | 
552 |
553 | 可用于添加系统信息,例如:db的配置信息,人员信息,环境信息等。根据项目实际情况添加。
554 |
555 | - 在src/main/java/reporter/config目录下创建MySystemInfo.java类,继承SystemInfo接口。
556 | ```
557 | public class MySystemInfo implements SystemInfo {
558 | @Override
559 | public Map getSystemInfo() {
560 | InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
561 | Properties properties = new Properties();
562 | Map systemInfo = new HashMap<>();
563 | try {
564 | properties.load(inputStream);
565 | systemInfo.put("environment", properties.getProperty("Environment"));
566 | systemInfo.put("sqlURL", properties.getProperty("ESsql.URL"));
567 | systemInfo.put("redisHost", properties.getProperty("redis.host"));
568 | systemInfo.put("redisPort", properties.getProperty("redis.port"));
569 | systemInfo.put("mongodbHost", properties.getProperty("mongodb.host"));
570 | systemInfo.put("mongodbPort", properties.getProperty("mongodb.port"));
571 | systemInfo.put("测试人员", "jxq");
572 | } catch (IOException e) {
573 | e.printStackTrace();
574 | }
575 | return systemInfo;
576 | }
577 | }
578 | ```
579 |
580 | 至此,extentreports美化报告完成。
581 |
582 |
583 | ### (六). retrofit2.0--Http接口测试驱动原力
584 | > 其实Java的Http客户端有很多,例如HTTPClient、OKHttp、retrofit等。。。
585 | > 为什么那么多Http客户端会选择retrofit?用一个图见证他的实力
586 |
587 | 
588 |
589 | > 真正的原因
590 | **接口定义与实现分离**
591 | retrofit2.0可将Http接口定义与请求实现分离;通过制定interface定义接口。
592 | 网上有很多关于retrofit2.0的教程,这里就不再班门弄斧了,度娘即可。参考:https://blog.csdn.net/carson_ho/article/details/73732076
593 |
594 | 附上本项目方式。
595 |
596 | ##### 6.1 具体Http Api的定义interface。新建ISearch interface。
597 | ```
598 | public interface ISearch {
599 | @GET("j/search_tags")
600 | Call searchTags(@Query("type") String type, @Query("source") String source);
601 | }
602 | ```
603 |
604 | ##### 6.2 HttpBase基础类提供原动力。
605 | HttpBase类中提供了Retrofit基础。
606 | 同时,我考虑到了日常控制台和测试报告上都需要看到对应请求信息,故此在HttpClient中默认加入了日志拦截器;日志拦截器的实现方法里,用Reportes.log记录到日志中。
607 | 并且,考虑到实际项目中每个Http请求都会有对应类似RequestHeader、RequestBody的加密签名等,预留了拦截器。
608 | 可在HttpBase构造方法时传入对应拦截器。
609 | 对应的拦截器可以通过实现接口Interceptor,做对应项目需求操作。
610 | 先看代码。
611 | ```
612 | public class HttpBase {
613 | public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
614 | private Retrofit retrofit;
615 | private String host;
616 |
617 | /**
618 | * 构造方法(1个参数)
619 | * 只传Host,默认没有使用拦截器。
620 | *
621 | * @param host 访问域名host
622 | */
623 | public HttpBase(String host) {
624 | init(host, null);
625 | }
626 |
627 | /**
628 | * 构造方法(2个参数)
629 | * 只传Host,默认使用日志拦截器。
630 | *
631 | * @param host 访问域名host
632 | * @param interceptor 自定义拦截器
633 | */
634 | public HttpBase(String host, Interceptor interceptor) {
635 | init(host, interceptor);
636 | }
637 |
638 | /**
639 | * 初始化方法
640 | *
641 | * @param host 访问域名host
642 | * @param interceptor 自定义拦截器
643 | */
644 | private void init(String host, Interceptor interceptor) {
645 | OkHttpClient.Builder client = getHttpClient(interceptor);
646 | retrofit = new Retrofit.Builder()
647 | .baseUrl(host)
648 | .client(client.build())
649 | .addConverterFactory(RespVoConverterFactory.create())
650 | .build();
651 | }
652 |
653 | /**
654 | * 获取HttpClient.Builder 方法。
655 | * 默认添加了,基础日志拦截器
656 | *
657 | * @param interceptor 拦截器
658 | * @return HttpClient.Builder对象
659 | */
660 | private OkHttpClient.Builder getHttpClient(Interceptor interceptor) {
661 | HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
662 | OkHttpClient.Builder builder = new OkHttpClient.Builder()
663 | .connectTimeout(10, TimeUnit.SECONDS)
664 | .retryOnConnectionFailure(true);
665 | if (interceptor != null) {
666 | builder.addInterceptor(interceptor);
667 | }
668 | builder.addInterceptor(logging);
669 | return builder;
670 | }
671 |
672 | /**
673 | * 日志拦截器
674 | *
675 | * @return
676 | */
677 | private HttpLoggingInterceptor getHttpLoggingInterceptor() {
678 | HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
679 | @Override
680 | public void log(String message) {
681 | Reporter.log("RetrofitLog--> " + message, true);
682 | }
683 | });
684 | logging.setLevel(HttpLoggingInterceptor.Level.BODY);//Level中还有其他等级. 设置打印内容级别到Body。
685 | return logging;
686 | }
687 |
688 | /**
689 | * retrofit构建方法
690 | *
691 | * @param clazz 泛型类
692 | * @param 泛型类
693 | * @return 泛型类
694 | */
695 | public T create(Class clazz) {
696 | return retrofit.create(clazz);
697 | }
698 |
699 | public String getHost() {
700 | return host;
701 | }
702 |
703 | public void setHost(String host) {
704 | this.host = host;
705 | }
706 | }
707 | ```
708 |
709 |
710 | ##### 6.3 集成HttpBase的Http Api接口请求方法类
711 | 这里需要说明下,为什么需要有这个类的存在?
712 | 其实在Retrofit已经可以用4行的代码实现Http请求了,如下:
713 | ```
714 | HttpBase httpBase = new HttpBase(host);
715 | ISearch iSearch = httpBase.create(ISearch.class);
716 | Call call = iSearch.searchTags(type, source);
717 | Response response = call.execute();
718 | ```
719 | 看了上面的4行代码,每次都需要写也是挺麻烦的。
720 | 所以抽出来,让编写测试用例验证更简洁点。
721 | 抽取后的代码如下:
722 | ```
723 | public class HttpSearch extends HttpBase {
724 | private ISearch iSearch;
725 |
726 | public HttpSearch(String host) {
727 | super(host);
728 | iSearch = super.create(ISearch.class);
729 | }
730 |
731 | public Response searchTags(String type, String source) throws IOException {
732 | Call call = iSearch.searchTags(type, source);
733 | return call.execute();
734 | }
735 |
736 | // 同模块下,新增的接口可添加到这里。
737 | // public Response searchTags(String type, String source) throws IOException {
738 | // Call call = iSearch.searchTags(type, source);
739 | // return call.execute();
740 | // }
741 | }
742 | ```
743 |
744 | ##### 6.4 使用JsonSchema验证基础响应体
745 | > *Http响应体非Json格式,可跳过该步骤。*
746 |
747 | 这里引入了JsonSchema来做基础验证,减少了Http响应返回带来的大量对象基础验证。
748 | 方式如下:
749 | - 6.5.1 pom.xml 依赖引入
750 | ```
751 |
752 |
753 |
754 | com.fasterxml.jackson.core
755 | jackson-core
756 | 2.9.6
757 |
758 |
759 |
760 |
761 | com.fasterxml.jackson.core
762 | jackson-databind
763 | 2.9.6
764 |
765 |
766 |
767 | com.github.fge
768 | json-schema-validator
769 | 2.2.6
770 |
771 |
772 | ```
773 | - 6.5.2 简单抽象JsonSchemaUtils工具类。
774 | 直接看代码。
775 | ```
776 | /**
777 | * JsonSchema工具类
778 | */
779 | public class JsonSchemaUtils {
780 | /**
781 | * 从指定路径读取Schema信息
782 | *
783 | * @param filePath Schema路径
784 | * @return JsonNode型Schema
785 | * @throws IOException 抛出IO异常
786 | */
787 | private static JsonNode readJSONfile(String filePath) throws IOException {
788 | InputStream stream = JsonSchemaUtils.class.getClassLoader().getResourceAsStream(filePath);
789 | return new JsonNodeReader().fromInputStream(stream);
790 | }
791 |
792 | /**
793 | * 将Json的String型转JsonNode类型
794 | *
795 | * @param str 需要转换的Json String对象
796 | * @return 转换JsonNode对象
797 | * @throws IOException 抛出IO异常
798 | */
799 | private static JsonNode readJSONStr(String str) throws IOException {
800 | return new ObjectMapper().readTree(str);
801 | }
802 |
803 | /**
804 | * 将需要验证的JsonNode 与 JsonSchema标准对象 进行比较
805 | *
806 | * @param schema schema标准对象
807 | * @param data 需要比对的Schema对象
808 | */
809 | private static void assertJsonSchema(JsonNode schema, JsonNode data) {
810 | ProcessingReport report = JsonSchemaFactory.byDefault().getValidator().validateUnchecked(schema, data);
811 | if (!report.isSuccess()) {
812 | for (ProcessingMessage aReport : report) {
813 | Reporter.log(aReport.getMessage(), true);
814 | }
815 | }
816 | Assert.assertTrue(report.isSuccess());
817 | }
818 |
819 | /**
820 | * 将需要验证的response 与 JsonSchema标准对象 进行比较
821 | *
822 | * @param schemaPath JsonSchema标准的路径
823 | * @param response 需要验证的response
824 | * @throws IOException 抛出IO异常
825 | */
826 | public static void assertResponseJsonSchema(String schemaPath, String response) throws IOException {
827 | JsonNode jsonSchema = readJSONfile(schemaPath);
828 | JsonNode responseJN = readJSONStr(response);
829 | assertJsonSchema(jsonSchema, responseJN);
830 | }
831 | }
832 | ```
833 | 这里已经将最后抽成简单方法供使用,只需传入schemaPath路劲、以及需要验证的对象。
834 |
835 | - 6.5.3 Http响应体保存到本地
836 | - ①、可以通过客户端抓包获取得到Http响应体、或者开发接口定义文档 等方式,得到最后Http响应体的Json对象。(注意:响应体内容尽量全面,这样在验证时也可以尽可能验证)
837 | - ②、将请求响应体通过 https://jsonschema.net/ ,在线验证得到JsonSchema信息。
838 | - ③、根据接口响应需求,做基础验证配置。例如,这里将tags字段认为是必须存在的参数。
839 | 完整Schema约束文件如下,并将此文件保存到resources目录对应模块下。
840 | ```
841 | {
842 | "$id": "http://example.com/example.json",
843 | "type": "object",
844 | "properties": {
845 | "tags": {
846 | "$id": "/properties/tags",
847 | "type": "array",
848 | "items": {
849 | "$id": "/properties/tags/items",
850 | "type": "string",
851 | "title": "The 0th Schema ",
852 | "default": "",
853 | "examples": [
854 | "热门",
855 | "最新"
856 | ]
857 | }
858 | }
859 | },
860 | "required": [
861 | "tags"
862 | ]
863 | }
864 | ```
865 |
866 | ##### 6.5 TestCase测试用例编写。
867 | ```
868 | public class SearchTagsTest {
869 | private static Properties properties;
870 | private static HttpSearch implSearch;
871 | private static String SCHEMA_PATH = "parameters/search/schema/SearchTagsMovie.json";
872 |
873 | @BeforeSuite
874 | public void beforeSuite() throws IOException {
875 | InputStream stream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
876 | properties = new Properties();
877 | properties.load(stream);
878 | String host = properties.getProperty("douban.host");
879 | implSearch = new HttpSearch(host);
880 | stream = this.getClass().getClassLoader().getResourceAsStream("parameters/search/SearchTagsParams.properties");
881 | properties.load(stream);
882 | stream = this.getClass().getClassLoader().getResourceAsStream("");
883 | stream.close();
884 | }
885 |
886 | @Test
887 | public void testcase1() throws IOException {
888 | String type = properties.getProperty("testcase1.req.type");
889 | String source = properties.getProperty("testcase1.req.source");
890 | Response response = implSearch.searchTags(type, source);
891 | MovieResponseVO body = response.body();
892 | Assert.assertNotNull(body, "response.body()");
893 | // 响应返回内容想通过schema标准校验
894 | JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
895 | // 再Json化成对象
896 | Assert.assertNotNull(body.getTags(), "tags");
897 | }
898 |
899 | @Test
900 | public void testcase2() throws IOException {
901 | String type = properties.getProperty("testcase2.req.type");
902 | String source = properties.getProperty("testcase2.req.source");
903 | Response response = implSearch.searchTags(type, source);
904 | MovieResponseVO body = response.body();
905 | Assert.assertNotNull(body, "response.body()");
906 | JsonSchemaUtils.assertResponseJsonSchema(SCHEMA_PATH, JSONObject.toJSONString(body));
907 | Assert.assertNotNull(body.getTags(), "tags");
908 | }
909 | }
910 | ```
911 | 至此,TestNg测试用例部分全部完成。
912 |
913 | ### 四、Jenkins部分配置
914 | > Jenkins的安装上面已有说明,这里不重复。
915 |
916 | #### (一) Jenkins插件
917 | ##### 1.插件列表
918 | 需要使用到的插件有:
919 | - Maven Integration plugin
920 | - HTML Publisher plugin
921 | - Dingding[钉钉] Plugin
922 | - TestNG Results
923 | - Groovy
924 | - Parameterized Trigger Plugin
925 |
926 | ##### 2. Jenkins插件安装
927 | 怎么安装插件?
928 | Jenkins-》系统管理-》插件管理-》搜索插件-》安装即可
929 |
930 | 
931 |
932 | ##### 3. 插件说明
933 | - Maven Integration plugin -必备!
934 | Maven构建插件,使用简单方便。
935 |
936 | - HTML Publisher plugin -必备!
937 | extentreporets美化报告替换testng就是为了好看,但要在jenkins中展示必须安装此插件。
938 |
939 | - Groovy -必备!
940 | Jenkins不支持异类样式CSS,所以Groovy插件是为了解决HTML Publisher plugin在展示extentreporets时能够正确美丽的作用。
941 |
942 | - Dingding[钉钉] Plugin -必备!
943 | 测试用例构建结果的通知。网上很多说用邮件,说实话使用场景最频繁高效的应该是IM靠谱。这个插件就是解决测试结果的通知。
944 |
945 | - TestNG Results - 非必备
946 | TestNg测试结果收集,统计运行结果数据。
947 |
948 | - Parameterized Trigger Pl ugin - 非必备
949 | 依赖构建传参插件。http://note.youdao.com/noteshare?id=c56333317d3078b36b2479fdf8fe68d7&sub=wcp1530172849180570
950 |
951 | #### (二)Jenkins新建任务配置
952 | 在插件安装完后,开始任务的新建配置。
953 | - 新建一个maven项目。
954 | 
955 |
956 | #### (三)General配置
957 | - 丢弃旧的构建配置 -可配
958 | 该配置根据需求配置。
959 | 
960 |
961 | #### (四) 构建配置-maven配置
962 | 在Jenkins使用Maven构建项目自动化测试前,先通过本地使用maven测试是否通过。
963 | 这里本来要将参数化构建,但参数化构建前先说明下是如何利用maven构建测试的。
964 |
965 | - 1. 检查pom.xml配置中指定的suiteXmlFile对象。
966 | ```
967 |
968 |
969 | ${project.basedir}/target/classes/testNg/${xmlFileName}
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 | ```
979 |
980 | - 2. 先在IDEA上验证maven test是否生效?
981 |
982 | 在```${project.basedir}/target/classes/testNg/api/testng.xml``` 开启后,使用maven test验证是否成功。如下图:
983 |
984 | 
985 |
986 | - 3. 通过terminal命令验证maven test是否生效
987 | - 在2.3.1验证通过后,pom.xml注释```${project.basedir}/target/classes/testNg/api/testng.xml```.
988 | - 打开```${project.basedir}/target/classes/testNg/${xmlFileName}```
989 | - 进入terminal命令验证maven test是否生效。在命令行上输入```mvn clean test -DxmlFileName=testng.xml```
990 | - 验证maven test 是否正确。
991 |
992 | 
993 |
994 | - 4. 唠叨编码问题
995 | > 我在执行上面的命令时,maven一直提示警告信息-编码问题;该警告信息原先我本不太在意,因为配置没有问题。
996 | > 可后来,命令执行一直报错。看了报错信息都指向了非编码问题。也就把我引向了其他错误解决区域。
997 | > 不得不说,maven的提示还是要重头到尾认真看。因为真正报错误的地方不一定是[error]提示。
998 | > 警告信息是: [WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
999 |
1000 | 如何解决该问题呢?在pom.xml上加入如下配置。
1001 |
1002 | ```
1003 | // 这个配置由于被误删了,导致花费了半天解决。。。
1004 |
1005 | UTF-8
1006 |
1007 |
1008 | ```
1009 |
1010 | #### (五) 配置构建-maven配置信息。
1011 | 回到Jenkins界面配置maven信息。
1012 | - 5.1 先在Jenkins新建任务,构建模块,增加构建步骤 - 调用顶层maven目标。
1013 | (需要注意下,这里写的是中文,根据不同的Jenkins版本该名称可能是英文的)
1014 | 
1015 |
1016 | - 5.2 然后配置信息如下。在目标加入命令信息:clean test -P%env% -DxmlFileName=%xmlFileName%
1017 | 
1018 |
1019 | - 5.3命令解释:clean test -P%env% -DxmlFileName=%xmlFileName%
1020 | > - maven参数化替换使用的占位符是 %xxx%
1021 | > - -P%env% 指定maven运行的环境,该环境信息与pom.xml 配置的信息一直。同时,-P%env% 用于参数化构建传参使用,后面会有介绍。
1022 | > - -DxmlFileName=%xmlFileName% 指定maven test 运行的测试集合对象。用于参数化构建传参使用,后面介绍。
1023 |
1024 | #### (六) 参数化构建过程 配置
1025 | - 6.1添加参数 选择是 【选项参数】。
1026 | 
1027 |
1028 | - 6.2完整配置信息如下图。
1029 | 
1030 |
1031 | - 6.3参数名称xmlFileName,对应maven构建中的-DxmlFileName=%xmlFileName%,再对应pom.xml中的```${project.basedir}/target/classes/testNg/${xmlFileName}```
1032 | 加入需要运行的集合选项。
1033 | - 6.4 同样,env对应maven构建中的 -P%env% ,再对应pom.xml中的build信息。
1034 | 加入运行的环境选项。
1035 |
1036 | #### (七) 源码管理配置
1037 | 这个配置网上有很多详细文档,这里不重复。具体度娘查看。
1038 | 
1039 |
1040 | #### (八) 构建触发器
1041 | > 这个配置可根据实际项目需求配置。个人建议: 接口自动化测试中的自动化最核心的是结合持续构建。
1042 | > 所以建议配置“其他工程构建后触发”,填入所需测试的服务端项目名称即可。当然要在一个Jenkins中。
1043 | 
1044 |
1045 | ### (九) 构建信息配置
1046 | > 上面已经配置了“调用顶层Maven目标”,然后还需要配置Groovy script。
1047 | > 配置Groovy script的目的是让Http Reported插件css能用,同时不用担心jenkins重启。
1048 |
1049 | - 配置Groovy script前保障Groovy 插件已经安装。
1050 | - 增加构建步骤“Execute system Groovy script” ,选择Groovy command,填入```System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")```
1051 |
1052 | 
1053 |
1054 | ### (十) 构建后操作信息配置
1055 | ##### 9.1. publish html reports
1056 | 加入publish html reports步骤。
1057 | - HTML directory to archive: 报告路径。 填写extentreports默认输出路径:test-output\
1058 | - Index page[s] : 报告索引名称。填写extentreports默认报告名称:report.html
1059 | - Keep past HTML reports: 保留报告,勾选!不多说。
1060 | 
1061 |
1062 | ##### 9.2 publish html reports
1063 | publish testng results 配置。默认**/testng-results.xml 即可。
1064 | 为什么要testng默认报告? 因为需要统计分析时查看。 当然这个是可选的。
1065 | 
1066 |
1067 | ##### 9.3. 钉钉通知器配置
1068 | 怎么玩转钉钉消息?查看https://blog.csdn.net/workdsz/article/details/77531802
1069 | - 填入access token。
1070 | 
1071 |
1072 | ##### 4. 构建后操作信息配置 钉钉通知器配置 二次开发 - 可选
1073 | http://www.51testing.com/html/25/n-3723525.html
1074 |
1075 | ### (十一) 构建测试
1076 | - 11.1 build with parameters
1077 | 
1078 |
1079 | - 11.2 构建成功后 在 HTML Report上查看
1080 | 
1081 | 
1082 |
1083 | - 11.3 构建成功后 在 TestNG Results上查看
1084 | 
1085 |
1086 | - 11.4 构建成功后 在 钉钉上查看
1087 | 
1088 |
1089 |
1090 | ### 五、工程目录讲解 与 接口测试用例编写步骤
1091 | #### (一) 工程目录讲解
1092 | 先上图说明
1093 | 
1094 |
1095 | 
1096 |
1097 | #### (二) 接口测试用例编写步骤
1098 | ##### 1. 目标:具体Htpp接口定义的熟悉、理解。
1099 | 注意:Http 请求行、请求头、请求体;响应行、响应头、响应体
1100 | - 可通过查看开发wiki文档,或者通过抓包等手段达到。
1101 |
1102 | ##### 2. 具体Htpp接口的定义 - Host
1103 | - 2.1 将不同环境的Host地址配置在env.properties文件
1104 | - 2.2 然后在根据不同环境,配置不同properties文件,中对应的host信息。filter-debug.properties、filter-dev.properties、filter-product.properties。
1105 |
1106 | ##### 3. 具体Htpp接口的定义 - interface
1107 | - 3.1 在src/main/java/下com.xxx.api.下新建对应模块,例如article
1108 | - 一个模块文件夹下存放:接口定义的interface、接口定义的实现类。
1109 | - 3.2 在article下新建具体的接口定义interface
1110 | ```
1111 | public interface IArticle {
1112 | @POST("article/feed")
1113 | Call articleFeed(@Query("tid") String tid, @Body RequestBody requestBody);
1114 | }
1115 | ```
1116 |
1117 | ##### 4. 具体Htpp接口的定义实现 - implement
1118 | - 4.1 在article下新建具体的接口定义实现
1119 | ```
1120 | public class ImplArticle {
1121 | public static String articleFeed(String host, String tid, String requestBody) throws IOException {
1122 | Retrofit retrofit = new Retrofit.Builder()
1123 | .baseUrl(host)
1124 | .build();
1125 | IArticle iArticle = retrofit.create(IArticle.class);
1126 | Call call = iArticle.articleFeed(tid, RequestBody.create(CallHttp.JSON, requestBody));
1127 | return CallHttp.doCall(call, request);
1128 | }
1129 | }
1130 | ```
1131 |
1132 | ##### 5. 编写测试用例 -xxxTest
1133 | - 5.1 在test/java 目录下新建对应测试模块的文件夹 ,例如:article
1134 | - 一个接口用例类,对应一个文件夹。
1135 | - 5.2 在test/java/article 目录下新建测试用例类。
1136 | - 开始编写接口测试类。
1137 |
1138 | ```
1139 | public class ArticleFeedTest {
1140 | private static String HOST;
1141 | private static Properties properties;
1142 |
1143 | @BeforeSuite
1144 | public void beforeSuite() throws IOException {
1145 | InputStream stream = this.getClass().getClassLoader().getResourceAsStream("env.properties");
1146 | properties = new Properties();
1147 | properties.load(stream);
1148 | HOST = properties.getProperty("api.newsapi.host");
1149 | stream = this.getClass().getClassLoader().getResourceAsStream("parameters/api/article/ArticleFeedParam.properties");
1150 | properties.load(stream);
1151 | stream.close();
1152 | }
1153 |
1154 | @Test
1155 | public void testcase1() throws IOException {
1156 | String reques = properties.getProperty("testcase1.requestBody");
1157 |
1158 | String response = ImplArticle.articleFeed(HOST, "tid", reques);
1159 | ResponseBodyVo responseBodyVo = JSONObject.parseObject(response, ResponseBodyVo.class);
1160 | assertResponseBody(responseBodyVo);
1161 | }
1162 | }
1163 | ```
1164 |
1165 | ##### 6. 具体接口测试用例suite集合制作
1166 | - 在testNg/api/article/ArticleFeed-TestSuite.xml下创建suite集合
1167 |
1168 | ```
1169 |
1170 |
1171 |
1172 |
1173 |
1174 |
1175 |
1176 |
1177 |
1178 |
1179 |
1180 |
1181 |
1182 |
1183 |
1184 |
1185 |
1186 |
1187 |
1188 |
1189 | ```
1190 |
1191 | ##### 7. 所有接口测试用例suite集合制作
1192 | - 在testNg/api/APICollection-TestSuite.xml下创建suite集合
1193 | ```
1194 |
1195 |
1196 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 |
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 |
1210 |
1211 |
1212 |
1213 | ```
1214 |
1215 | #### 写在最后
1216 | 其实,接口自动化测试平台的搞起来不难。
1217 | 推动平台接入到持续集成,将测试变成一种服务,更快更及时的服务于项目,才是重点。
1218 | 正所谓:wiki一定,开发未动,接口已行。
1219 | 而,服务端测试才挑战。知识储备的深度决定了,测试的深度。
1220 |
1221 | 个人GitHub: https://github.com/Jsir07/TestHub
1222 | 欢迎Watch + Fork
1223 | end...
1224 |
1225 |
1226 |
--------------------------------------------------------------------------------