敏捷测试高效秘诀:GraphQL API模拟服务器的搭建与优化

2753 篇文章 2 订阅
2589 篇文章 14 订阅

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, 数据字符型
当服务器接收到请求时,需要返回用户是否可以调阅文档以及其他基本信息

  1.  返回数据结果可以调阅文档是否,需要按预置条件

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%免费】

在这里插入图片描述

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值