├── 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 | ![Jenkins自助运行流程](https://upload-images.jianshu.io/upload_images/1592745-11690347752e023f.GIF?imageMogr2/auto-orient/strip) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-e1794c63508719de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-cecb4b9d26f51ee5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 101 | 102 | ##### 1.2 填写对应项目信息后,next。 103 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-d7d63d3d9a86f7b2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 104 | ##### 1.3 继续填写 对应信息后,Finish。 105 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-a2b383a44993315a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-2b03ef30d695a899.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-caffacaa53a55671.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-db9bbb5ef50eb56b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![如此多的星星可知](https://upload-images.jianshu.io/upload_images/1592745-4e75bc87d014a097.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-c726c78951c2ccd1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-9d512595d8e7b610.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 955 | 956 | #### (三)General配置 957 | - 丢弃旧的构建配置 -可配 958 | 该配置根据需求配置。 959 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-f1e48f5e13c01fa0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-d69bb8f5b7550773.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-06c56c65faaf9bea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-7c90fe29582692cc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1015 | 1016 | - 5.2 然后配置信息如下。在目标加入命令信息:clean test -P%env% -DxmlFileName=%xmlFileName% 1017 | ![](https://upload-images.jianshu.io/upload_images/1592745-f0d0de23bb5a72ca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-0486fbca91a38367.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1027 | 1028 | - 6.2完整配置信息如下图。 1029 | ![](https://upload-images.jianshu.io/upload_images/1592745-f586ae3356c5dd3b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-f6d102928da390fe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1039 | 1040 | #### (八) 构建触发器 1041 | > 这个配置可根据实际项目需求配置。个人建议: 接口自动化测试中的自动化最核心的是结合持续构建。 1042 | > 所以建议配置“其他工程构建后触发”,填入所需测试的服务端项目名称即可。当然要在一个Jenkins中。 1043 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-5c69a2115ce3327c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-9a3742f57a4ed0d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | ![](https://upload-images.jianshu.io/upload_images/1592745-9143ff5424476c8d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1061 | 1062 | ##### 9.2 publish html reports 1063 | publish testng results 配置。默认**/testng-results.xml 即可。 1064 | 为什么要testng默认报告? 因为需要统计分析时查看。 当然这个是可选的。 1065 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-9625b6b2ec2e35df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1066 | 1067 | ##### 9.3. 钉钉通知器配置 1068 | 怎么玩转钉钉消息?查看https://blog.csdn.net/workdsz/article/details/77531802 1069 | - 填入access token。 1070 | ![image.png](https://upload-images.jianshu.io/upload_images/1592745-4e1ca2a4d34564d4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1071 | 1072 | ##### 4. 构建后操作信息配置 钉钉通知器配置 二次开发 - 可选 1073 | http://www.51testing.com/html/25/n-3723525.html 1074 | 1075 | ### (十一) 构建测试 1076 | - 11.1 build with parameters 1077 | ![](https://upload-images.jianshu.io/upload_images/1592745-c509b41047965ce8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1078 | 1079 | - 11.2 构建成功后 在 HTML Report上查看 1080 | ![](https://upload-images.jianshu.io/upload_images/1592745-6a2c01b7815cc05f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1081 | ![](https://upload-images.jianshu.io/upload_images/1592745-acc148199c4dd3fc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1082 | 1083 | - 11.3 构建成功后 在 TestNG Results上查看 1084 | ![](https://upload-images.jianshu.io/upload_images/1592745-f20e70dab17521d4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1085 | 1086 | - 11.4 构建成功后 在 钉钉上查看 1087 | ![](https://upload-images.jianshu.io/upload_images/1592745-08bc711f90a9fada.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1088 | 1089 | 1090 | ### 五、工程目录讲解 与 接口测试用例编写步骤 1091 | #### (一) 工程目录讲解 1092 | 先上图说明 1093 | ![](https://upload-images.jianshu.io/upload_images/1592745-009769b3a99eca47.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1094 | 1095 | ![](https://upload-images.jianshu.io/upload_images/1592745-a79305d5a042c8e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 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 | --------------------------------------------------------------------------------