1 学习目的
敏捷开发,数据横行的时代,测试的要求越来越重要,复杂。在接受挑战和变化时,测试团队需要不断学习和适应新的技术和方法。
本例我们将一步步搭建一个 jetty-server 实现 Graphql API 的请求与返回,然后通过Postman 讲解Graphql API 请求的2 种请求方式,最后通过rest-Assured在testNg框架下模拟API 的请求与数据返回的测试,最终实现Graphql API在模拟服务器上测试的自动化。
2 温馨提示
如果您要按着示例一起做,请务必配置如下工具以及学习相关的知识。用例主要是搭建Graphql API 服务器,结合rest-Assured完成API 的自动化测试,对于不了解rest-Assured ,Graphql API的小伙伴,请做些额外功课吧(可以参考我之前写的rest-Assured的文章,有详细的配置与应用步骤)。
-
IDE: IntelliJ IDEA
-
语言:Java
-
API 测试开发:Rest-Assured
-
API :Graphql API
-
测试框架:TestNg
-
项目类型:Maven
-
API 测试工具:Postman
3 知识重点
-
Graphql API服务器搭建:创建Graphql API Schema, 根据参数灵活返回请求
-
Graphql API 请求:理解Graphql API请求方式通过Postman
-
Rest Assured :模拟API 请求并验证
01 Maven项目配置
配置构建Graphql API 服务器,rest-Assured 插件在POM.xml
如下所示:需要将 对应的插件正确的引入到 pom.xml 的节点下。(本次用例的插件比较多哦) 提示:如果dependency没有自动加载,可以手动加载,相应的jar 包就会下载了。
Graphql
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>22.0</version>
</dependency>
Jetty server 以及相关插件
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>11.0.21</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>11.0.21</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>11.0.21</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
rest-Assured
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>compile</scope>
</dependency>
02 测试用例分解
Graphql API:
首先了解一下要搭建的服务器的接收数据参数、类型以及返回的数据类型、格式,下图postman 中显示了graphql api 的请求与数据返回格式。想像一下有这样一个服务器,当用户请求调阅某一个文档时,服务器就会返回对应的用户信息以及是否可以调阅访文档的信息。
API数据分析:
首先要搭建一个接受API的服务器,并且能够对有符合条件的API请求做出响应。
地址:http://localhost:8080/graphql
参数:name,professional,fileName
参数格式:字符型
响应:department,name,web,level
响应格式与类型:格式 .json, 数据字符型
当服务器接收到请求时,需要返回用户是否可以调阅文档以及其他基本信息
-
返回数据结果可以调阅文档是否,需要按预置条件
professional 是 staff 可以查阅非机密文档
professional 是manager 可以查阅 任何类型的文档
如果定义用户调阅 的文档是 否机密呢?因为本例主要是学习graphql 服务器的搭建,所以通过fileName验证是否为机密文档。如果 fileName 包含 “confidential”就判定该文档为机密,否则就是非机密文档。
有了上面的理解,现在就是实现步骤:
1. 构建API 服务器 (定义请求,返回数据,启动服务,关闭服务)
2. 验证API 是否成功 (通过postman)
3. 自动测试 – rest-Assured 请求并验证返回状态与调阅权限
提示:这一步非常的重要,使得后续的服务器的搭建、测试条理化,能系统性的、逻辑性的组织工作任务。
03 构建GraphQL API服务器
此用例中构建服务器需要2个java 文件:
1.MockGraphQLServer.java -
此文件主要功能是定义 API的schema,定义数据返回内容依据请求参数 , 以及服务器的开启与关闭。
schemaBuilder()方法定义并返回 API schema, 以下代码 有2 个object,staffLevelType对象 定义了响应数据应该有的字段和字段的类型。queryType 对象定义了请求时的需要的参数以及参数类型。
public GraphQLSchema schemaBuilder() {
// define staff level type (response filed , type)
GraphQLObjectType staffLevelType = GraphQLObjectType.newObject()
.name("StaffLevel")
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("department").type(GraphQLString))
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("name").type(GraphQLString))
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("web").type(GraphQLString))
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("level").type(GraphQLString))
.build();
// define query parameter type
GraphQLObjectType queryType = GraphQLObjectType.newObject()
.name("Query")
.field(GraphQLFieldDefinition.newFieldDefinition()
.name("getStaffLevel")
.type(new GraphQLList(staffLevelType))
.argument(argument -> argument.name("name").type(GraphQLString))
.argument(argument -> argument.name("professional").type(GraphQLString))
.argument(argument -> argument.name("fileName").type(GraphQLString))
.dataFetcher(getStaffLevelDetails())) // get response data
.build();
GraphQLSchema schema = GraphQLSchema.newSchema().query(queryType).build();
return schema;
}
getStaffLevelDetails()方法是对请求参数的具体数据返回。这里为了比较直观地说明返回数据与请求参数的验证关系,显示数据 定义的details 就直接 写在了每个判断条件里,这样的结果是代码看起来不专业,但是容易简单明了地知道业务逻辑关系,调试也比较容易。在实际开发中,待业务确定以后,就可以优化这部分代码。
private DataFetcher<?> getStaffLevelDetails() {
return environment -> {
String name = environment.getArgument("name");
String professional = environment.getArgument("professional");
String fileName = environment.getArgument("fileName");
List<Map<String, Object>> staffLevelList = new ArrayList<>();
if (professional.contentEquals("manager")) {
Map<String, Object> details = new LinkedHashMap<>();
details.put("department", "LAB A");
details.put("name", name);
details.put("web", "http://www." + name + ".com");
details.put("level", "high, your are allowed to review " + fileName);
staffLevelList.add(details);
} else {
if (fileName.contains("confidential")) {
Map<String, Object> details = new LinkedHashMap<>();
details.put("department", "LAB A");
details.put("name", name);
details.put("web", "http://www." + name + ".com");
details.put("level", "low, you are not allowed to review confidential file " + fileName);
staffLevelList.add(details);
} else {
Map<String, Object> details = new LinkedHashMap<>();
details.put("department", "LAB A");
details.put("name", name);
details.put("web", "http://www." + name + ".com");
details.put("level", "low, your are allowed to review none confidential file " + fileName);
staffLevelList.add(details);
}
}
return staffLevelList;
};
}
服务开启与关闭
下面这2个 方法就很容易理解了,把 port 作为一个参数是为了服务器启动的更灵活在测试中。
这里new GraphQLHandler(graphQL)是别一个GraphQLHandler.java里的构造方法。
public void serverStart(GraphQLSchema schema, int port) {
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
server = new Server(port);
server.setHandler(new GraphQLHandler(graphQL));
try {
server.start();
System.out.println("Local server with port "+ port +" has been started");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void serverStop() {
if (server != null) {
try {
server.stop();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
2.GraphQLHandler .java -
此文件功能是处理API的请求,并对接受到请求进行优化再执行,当请求符合定义的 schema格式时返回请求成功数据,另外此处也添加了用请求方法的验证。
1. GraphQLHandler Class extends AbstractHandler
2. 此处的 GraphQLHandler()方法被用在 服务器启动时。
3. handle()方法: 首先验证请求 的路径(/graphql) 与方法(post)
4. 要解释方法中具体的功能,先了解一下graphql api 请求方式。 在接下的postman 使用中我们详细讲解。
而这里需要了解的是 graphql api 的请求分2 部分,一部分是 查询,一部分是 参数的变量值。在执行时,代码不能按一个整体发送,必须拆分成2部分然后在重定义成 executionInput 才可以请求成功。在每一个步骤里我都加了注释,期望。
public class GraphQLHandler extends AbstractHandler {
private final GraphQL graphQL;
public GraphQLHandler(GraphQL graphQL) {
this.graphQL = graphQL;
}
@Override
public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException {
try {
if ("/graphql".equalsIgnoreCase(s) && "POST".equalsIgnoreCase(request.getMethod())) {
// get request body from request
String requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
// covert request boy as jsonObject
JsonObject jsonObject = JsonParser.parseString(requestBody).getAsJsonObject();
// extract query part from jsonObject
String query = jsonObject.get("query").getAsString();
// extract variables part from jsonObject
JsonObject variablesObj = jsonObject.getAsJsonObject("variables");
// reform variables as map type
LinkedHashMap<String, Object> variables = new LinkedHashMap<>();
variablesObj.entrySet().forEach(entry ->{
variables.put(entry.getKey(),entry.getValue().getAsString());
});
// covert query with variables as execution input
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build();
// perform execution
ExecutionResult executionResult = graphQL.execute(executionInput);
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
// reform returned data as json
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(httpServletResponse.getWriter(),executionResult.toSpecification());
request.setHandled(true);
}
}catch (Exception e)
{
e.printStackTrace();
httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
httpServletResponse.getWriter().println("Internal Server Error");
request.setHandled(true);
}
}
}
04 Postman请求验证
1. 启动服务器, 在MockGraphQLServer.java 文件我们创建 一个 main 方法,在此方法调用 schemaBuilder() 和serverStart() , 点击运行这个方法。
在控制台会看到 启动成功的信息:local server with port 8080 has been started
2. Postman 请求方法一: GraphQL
打开postman, 在主功能菜单下选择 文件 > 新建 , 在弹出的窗口下选择 并创建 GraphQL.
输入地址: http://localhost:8080/graphql 等待加载成功。在query 部分可以看到 getStaffLevel (StaffLevel代码中定义部分) 的各个字段以及对应的字段类型。其中可以name,professional,fileName 是3个变量。选择每个字段并 输入相应的参数, 点击Query 按钮。
按示例中,professional: manager, 可以调阅所有的文件。返回的数据格式json。
3. Postman 请求方法二:Http
打开postman, 在主功能菜单下选择 文件 > 新建 , 在弹出的窗口下选择 并创建 HTPP.
输入地址:http://localhost:8080/graphql , 请求方式:POST. 在Body > GraphQL > 输入 Query 与GraphQL Variables 2 部分,点击 Send 按钮,验证返回结果在Body 部分。
从下面query 可以到 这里分2个部份,第1部分是参数(name,professional,fileName),第2部分是需要返回的数据(department,name,web,level) 。(注:这2个部分的字段可以调整顺序,同时也可以选择性取舍,但是不能去掉必填参数,或者返回字段是需求验证的。比如这个用例 中 fileName, professional 不可去掉,level 不可能去掉)
1 Query:
query StaffLevel($name: String, $professional: String, $fileName: String) { getStaffLevel(name: $name, professional: $professional,fileName: $fileName) { department name web level }}
2 GraphQL Variables:
{
"name":"jun",
"professional":"staff",
"fileName":"projectA_design_confidential.doc"
}
现在理解了 Graphql Api 的http 查询方式,那么在前面 构建服务器 GraphQLHandler.java 里的handle()方法,所谓的不能用一个整体发送这个查询,就是指查询与变量不能是一个字符串的整体,而是要Query 与GraphQL Variables 分解再组合成 executionInput当接收到 rest-Assured 的请求时。
05 TestNg 用例
依照上面分析,让我们一步一步看下代码的实现吧。
提示:本示例用的Java + testNg + rest-assured (如若对这个组合有点陌生的小伙伴,可以看看我相关的基础文章)
代码详解:GraphQLTest.java
1. 基于前面 http 查询的请求详解,requestBody()方法是对Query与 GraphQL Variables 组合并返回。
ublic String requestBody(String name, String professional,String fileName){
String query = "query StaffLevel($name: String, $professional: String, $fileName: String)" +
" { getStaffLevel(name: $name, professional: $professional,fileName: $fileName)" +
" { department" +
" name" +
" web" +
" level" +
" }" +
"}";
return "{\"query\":\""+query+"\",\"variables\":{\n" +
"\"name\":\""+name+"\"," +
"\"professional\":\""+professional+"\"," +
"\"fileName\":\""+fileName+"\" }}";
}
2. test_mock()方法分4部分 1)服务器启动 baseURI 设置; 2)GraphQL Variables 定义赋值与request请求 ; 3) response 数据验证 ; 4) 服务器关闭
@Test
public void test_mock(){
// setup server and baseURI
MockGraphQLServer graphQLServer = new MockGraphQLServer();
GraphQLSchema schema = graphQLServer.schemaBuilder();
int port = 808;
graphQLServer.serverStart(schema, port);
baseURI = "http://localhost:"+port+"/graphql";
// query
String name="jun";
String professional="staff";
String fileName="projectA_design_confidential.doc";
String requestBody = requestBody(name,professional,fileName);
Response response = given().contentType("application/json").body(requestBody).log().all().post();
// verify
response.then().statusCode(200);
String verifyLevelMessage = "low, you are not allowed to review confidential file "+ fileName;
response.then().body("data.getStaffLevel[0].level",containsString(verifyLevelMessage));
response.getBody().prettyPrint(); // print out response
//stop server
graphQLServer.serverStop();
}
06 用例运行
好了,先在运行下测试结果吧。这里我就直接在IDE里运行了。为更好地捕获请求与返回时的具体信息: 在request与response中都应用了.log().all()
response.then().log().all().statusCode(200)
07 错误排查与代码优化
问题一:InvalidSyntax
在搭建服务器时我遇到的最头疼、最费时解决的一个错误信息就是 InvalidSyntax(如下图所示)
这个运行中,executionInput 是由requestBody直接创建的。
// directly use requestBody
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(requestBody).build()
在排除这个问题时,起初以为是请求Body的格式写错了,就转到请求真实的服务器进行验证。然后以为是 schema 定义的错误,反复检查重写这部分代码。最后认为是 handle()功能出的错误,但是定义不到具体的出错代码,不知道原因,就束手无策。
一通的查询资料,问ChatGPT, 毫无结果,这个问题整整花费了5个多小时的不停地调试,最后才确定不能直接用requestBody而需要把Query与GraphQL Variables 分开。这里分享出来希望大家不要走弯路。
代码优化:除了上面提到定义返回数据的代码部分,小伙伴们也可以尝试进一步优化handle(),比如当接收到请求地址,方法错误时返回 bad request 等信息
08 总结
构建服务器主要是提前实现测试用例代码开发,一旦产品功能开发完成,测试就可以立即执行验证开发的功能是否符合要求。
不过这里有一个很严格的需求在文档与沟通方面,接口文档的定义包含数据字段、类型、格式以及业务要求都需要预先梳理清晰,开发与测试人员要对使用接口以及业务逻辑有明确且一致的理解。
如果服务器API 的开发方不在一个团队,测试人员还得需要与API开发方进行沟通交流,没有相关的知识,是无从构建一个模拟服务器的。在实际环境中,这也是很多公司忽略或者说在思想认知上标识为“没必要”。
测试工作范围可大可小,可深可浅,相信做测试的小伙伴们都有自己的理解,期望共勉。谢谢大家。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。