65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ZeroCode 基于 JSON 的开源测试框架进行 REST API 或 SOAP 测试自动化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (9投票s)

2018 年 5 月 3 日

Apache

9分钟阅读

viewsIcon

35631

使用 YAML/JSON 步骤的声明式、可配置且超级简单的 API 测试库

高层重点

  • 引言
  • 为什么它有用
  • 框架在后台如何工作(针对框架编写者)
  • 使用示例HelloWorld),调用一个简单的 GitHub API
  • 带有搜索和过滤功能的有用 JUnit 报告
  • 关注点(链接或编织之前的请求和响应)
  • 另请参阅 - 当您处于防火墙代理后面或需要 SSL 客户端时
  • Zerocode 的历史和动机
  • 为 Zerocode 做贡献
  • 谁在使用 Zerocode

 

它有助于将您的场景清晰地划分为微小的 YAML/JSON 步骤,这有助于在应用程序端到端集成测试消费者契约测试期间识别出具体是哪个环节失败通过

引言

Zerocode 通过消除重复的测试断言、HTTP 调用和有效负载解析代码,简化了 API 的测试和验证。请看一个示例。其强大的响应/有效负载比较和断言使测试周期变得非常简单和清晰。

例如,您下面的用户旅程或 ACs(验收标准)或一个场景

AC1
已知 - POST API 端点 '/api/v1/users' 用于创建用户,
当 - 我调用 API 时,
然后 - 我将收到 201 状态码以及用户 ID 和头部信息
并且 - 我将验证响应

AC2
已知 - REST API GET 端点 '/api/v1/users/${created-User-Id}',
当 - 我调用 API 时,
然后 - 我将收到 200 (Ok) 状态码以及主体(用户详细信息)和头部信息
并且 - 我将断言响应

将转换为 Zerocode 中的以下可执行 JSON - 简单而清晰!

请记住:它只是简单的 JSON。
没有特性文件,没有额外的插件,没有语句或语法开销。

post-get-param

它还有助于在测试周期中模拟/存根接口 API。它基于 IDE性能测试方法,用于对目标应用程序生成负载/压力,非常简单、灵活且高效——它更进一步,让您只需重用回归包中的测试。它为何有用,它解决了什么问题!

  • 您无需编写 POJO 或构建器来表示或创建您的测试输入/结果
    • 您只需您的有效负载的 YAMLJSON 等效项
  • 您无需编写代码来序列化或反序列化有效负载/响应
    • 框架会为您处理这一切
  • 所有响应断言不匹配都会一次性显示(见下图)
    • 它使循环变得简单,无论是纠正断言还是修复应用程序代码
  • 无论响应层次结构多么复杂,验证都无缝进行。您可以使用 Jayway JSON 路径验证整个响应原样 -或- 其中一部分 -或- 特定字段
    • 框架会为您处理。请参阅此处示例(观察步骤中的 verify 部分)。
  • 该框架允许您使用自己的自定义客户端(可选)来覆盖行为
    • 您只需使用 @UseHttpClientYourOwnCustomClient.class)。请参阅此处示例
    • 在下面的章节中也解释了如何覆盖。
  • Jenkins CI 集成对各种环境来说是无缝的
    • 只需在 Maven 的 -D 参数中传递 env 值。请参阅此处示例
  • JUnit 测试结果报告支持模糊文本匹配以及搜索和过滤
    • 作者过滤,或按场景名称过滤 -或- 按 通过/失败 状态过滤,等等。请参阅此处示例
  • 套件运行器是无缝的,您无需复制/粘贴一系列测试类名称
    • 只需指向测试包的根目录即可。请参阅此处示例
    • 可选地,可以复制/粘贴一系列类名作为 Junit 套件运行器
  • Zerocode readme 文件包含上述所有场景的示例以及更多用法示例

当断言失败时,即实际响应与“验证”块不匹配时,它会按 JSON 路径显示所有不匹配的字段(放大图片以获得清晰度),如下所示

Failure report

背景

在当今的自动化世界中,API 测试听起来足够简单,但我们是否以一种易于共享、易于维护和易于更改的方式进行这些(测试)?可能不是,因为太多的时间浪费在准备测试代码上,以便针对目标系统发出最终请求,然后太多的代码用于 assertassertThatassertNullassertTrue 等,并且这个循环还在继续,如果对象具有层次结构,那么序列化/反序列化/编写构建器/POJO 等方面的复杂性只会增加。即使我们采用这种方法,我们是否能够轻松共享 POJO/构建器/序列化器/反序列化器等,并向接口团队解释它们内部发生了什么?当测试失败时,调试是否变得容易?

消费者契约测试对于双方(服务提供者和服务消费者)都应该是可读和可维护的,它们应该看起来足够简单,并且必须包含请求、响应以可读的结构呈现给业务,因为我们与接口团队共享 JSON 契约。端到端集成测试也同样如此。

Zerocode 更进一步,使测试生命周期的这些方面变得如此简单,您将毫不费力地进行 BDD 和/或 TDD,但具有完全的清晰度,并以更高的效率和准确性专注于业务场景和 ACs(验收标准)

Using the Code

这是一个单步场景的示例,使用 @Test 注解将其作为 JUnit 运行。

//
// See the "verify" section below, if the response matches this JSON block, then the test will PASS
//
{
    "scenarioName": "Testing the GitHub REST end point",
    "steps": [
        {
            "name": "get_user_details",
            "url": "/users/octocat",
            "method": "GET",
            "request": {
                "queryParams":{
                   //key-value pair
                }
            },
            "retry": {
               "max": 3,
               "delay": 1000,
            }, 
            "verify": {
                "status": 200,
                "body": {
                    "login" : "octocat",
                    "id" : 583231,
                    "name" : "The Octocat",
                    "type" : "User"
                }
            }
        }
    ]
}

如何运行上述测试?

使用 IDE 中的 @Test 注解或如下 Maven 命令将其作为 JUnit 运行

$ mvn test -Dtest=org.jsmart.zerocode.testhelp.tests.helloworld#testGet

//
// In the JUnit test below point to the above file, 
// i.e., "helloworld/hello_world_status_ok_assertions.yml"
//
package org.jsmart.zerocode.testhelp.tests.helloworld;

import org.jsmart.zerocode.core.domain.JsonTestCase;
import org.jsmart.zerocode.core.domain.TargetEnv;
import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

@TargetEnv("github_host.properties")
@RunWith(ZeroCodeUnitRunner.class)
public class JustHelloWorldTest {

    @Test
    @JsonTestCase("helloworld/hello_world_status_ok_assertions.yml")
    public void testGetApi() throws Exception {
    }
}

框架如何工作?

让我们深入了解框架内部发生了什么。

  • 在上面的例子中,有以下注解
    • @RunWith(ZeroCodeUnitRunner.class)
    • @JsonTestCase("helloworld/hello_world_status_ok_assertions.yml")
    • @TargetEnv("github_host.properties")

下面我们来讨论一下。

@RunWith(ZeroCodeUnitRunner.class)

这个单元运行器 extends 并构建在 JUnit 核心运行器,即 BlockJUnit4ClassRunner 的基础上。框架源代码如下。请看下面的代码片段

public class ZeroCodeUnitRunner extends BlockJUnit4ClassRunner {
...
...
    @Override
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        ...
    }
...
...

}

以上代码片段内部发生了什么?

BlockJUnit4ClassRunner”的 runChild() 方法已在框架内部被覆盖,以便通过 @JsonTestCase 从 resources 文件夹中选择测试 JSON 文件。

选取测试用例后,它将测试场景分解为一个或多个步骤,每个步骤包含

  • url - REST 应用程序或 SOAP 服务器的 FQDN(完全限定域名),例如 https://api.github.com
  • method - 一个 Http 方法,例如 GET/PUT/POST/DELETE 等,所有 Apache Http Client 支持的方法。
  • request - 发送给被测目标系统/URL 的 JSON 请求。
  • retry - 最大重试次数,每次重试之间有一定延迟
  • queryParams - 用于过滤 REST API 输出的键值对
  • verify - 预期从服务器(即被测系统)收到的响应。

可选地,如果 URL 中使用了 Java 类,则以下分解生效,其中

  • url - 完全限定的 Java 类名
  • method - 上述 Java 类中的一个 public 方法
  • request - 传递给方法/操作的对象的 JSON 等效项
  • verify - 预期的返回值,Java public 方法输出的 YAML/JSON 等效项

注意:“request”、“verify”字段还支持 XML 输入,这有助于测试 SOAP 端点。此外,还可以选择将 XML 转换为 JSON 等效项(请参阅 README 中的 MIME 转换器),以便更易读、更精细地按字段进行断言。

...
...

    @Override
    public void run(RunNotifier notifier) {
        notifier.addListener(new ZeroCodeTestReportListener(...);
        super.run(notifier);
    }

...
...

这里,“BlockJUnit4ClassRunner”的“run()”方法已被覆盖,用于在所有测试运行完成后聚合测试结果。

一旦测试运行开始(通过 JUnit 运行器),payLoad 将从“request”字段中提取,并以完整路径调用 FQDN。

框架源代码如下

        ...
        executionResult = serviceExecutor.executeRESTService
                 (serviceName, operationName, resolvedRequestJson)
        ...
        final javax.ws.rs.core.Response serverResponse = 
           httpClient.execute(httpUrl, methodName, headers, queryParams, bodyContent);

@JsonTestCase("helloworld/hello_world_status_ok_assertions.yml")

此注解告诉“ZeroCodeUnitRunner”从 resources 中指定文件夹中选择测试用例。

            ...
            JsonTestCase annotation = method.getMethod().getAnnotation(JsonTestCase.class);

            if (annotation != null) {
                currentTestCase = annotation.value();
            } else {
                currentTestCase = method.getName();
            }
            ...

@TargetEnv("github_host.properties")

此注解包含被测目标应用程序的 host/port/context 属性。

restful.application.endpoint.host=https://api.github.com
restful.application.endpoint.port=443
restful.application.endpoint.context=

在框架内部,这些属性会附加到“url”中提到的相对路径,然后与 payLoad 一起被调用。在上面的示例中,它被解析为 https://api.github.com:443/users/octocat

这些属性通过“Google Guice”在框架内部进行绑定和注入,如下面的代码所示

    // serverEnv below is "github_host.properties" as annotated in the example

    public class ApplicationMainModule extends AbstractModule {
    ...

    @Override
    public void configure() {
        /*
         * Install other guice modules
         */
        ...
        install(new HttpClientModule());

        ...
        /*
		 * Bind properties for localhost, CI, SIT, PRE-PROD etc
		 */
        Names.bindProperties(binder(), getProperties(serverEnv));
    }

在 Java 方法执行的情况下,框架通过反射执行该方法,并返回一个等效的 JSON 作为响应。(请参阅框架源代码中的 >> org.jsmart.zerocode.core.engine.executor.JsonServiceExecutor,方法:executeJavaService())。

    ...
    //guice
    @Inject
    private JavaExecutor javaExecutor;

    public String executeJavaService(String serviceName, String methodName, String requestJson) 
                                     throws JsonProcessingException {

        if( javaExecutor == null) {
            throw new RuntimeException
                   ("Can not proceed as the framework could not load the executors. ");
        }

        List<Class<?>> argumentTypes = javaExecutor.argumentTypes(serviceName, methodName);

        try {
            Object request = objectMapper.readValue(requestJson, argumentTypes.get(0));
            Object result = javaExecutor.execute(serviceName, methodName, request);

            final String resultJson = objectMapper.writeValueAsString(result);

            return prettyPrintJson(resultJson);

        } catch (Exception e) {

            e.printStackTrace();

            throw new RuntimeException(e);

        }
    }
    ...

关注点

在上面的“步骤”部分,您可以使用 ${JSON Path to earlier steps} 添加多个步骤来构成一个场景,例如:请看下面

//
// See the "verify" section below, if the response matches this section, then the test will PASS
//
{
    "scenarioName": "GIVEN- the REST end points, WHEN- I invoke POST and GET, 
                     THEN- I will create and receive the new emp details",
    "steps": [
        {
            "name": "create_emp",
            "url": "/api/v1/google-uk/employees",
            "method": "POST",
            "request": {
                "body": {
                    "id": 1000,
                    "name": "Larry Pg",
                    "addresses": [
                        {
                            "gpsLocation": "x9000-y9000z-9000-home"
                        },
                        {
                            "gpsLocation": "x9000-y9000z-9000-home-off"
                        }
                    ]
                }
            },
            "verify": {
                "status": 201,
                "body": {
                    "id": 1000
                }
                // You can have more assertions here, but for now I am interested in 
                // whether the POST has created the "id" or not.
                // If the assertion fails here, then it will error out until 
                // you fix the target application.
          }
        },
        {
            "name": "get_user_details",
            "url": "/api/v1/google-uk/employees/${$.create_emp.response.body.id}", //reuse value
            "method": "GET",
            "request": {
            },
            "verify": {
                "status": 200,
                "body": {
                    "id": 1000,
                    "name": "Larry Pg",
                    "addresses": [
                        {
                            "gpsLocation": "${$.create_emp.response.body.addresses[0].gpsLocation}"
                        },
                        {
                            "gpsLocation": "x9000-y9000z-9000-home-off"
                        }
                    ]
                }
            }
        }
    ]
}

// The above example is available in the same HelloWorld demo project

另请参阅

好的,但是如果我想使用我自己的自定义 HttpClient 怎么办?

是的,事实上,作为开发人员或自动化测试人员,您经常需要拥有自己的 httpclient。您可以简单地用您自己的 HttpClient 覆盖框架提供的 HttpClient。这既简单又容易。请看下面的代码片段

完整源代码可在GitHub 上找到

import javax.ws.rs.core.Response;

public class SslTrustHttpClient implements BasicHttpClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(SslTrustHttpClient.class);
    ...
    @Override
    public Response execute(String httpUrl, String methodName, Map<String, Object> headers, 
                            Map<String, Object> queryParams, Object body) throws Exception {
        ...
    }
}

上述代码片段是做什么的?

它覆盖了框架 BasicHttpClient 的“public Response execute(...)”方法,并返回一个“javax.ws.rs.core.Response”。

在“execute(...)”方法内部,您只需使用自己的“custom HttpClient”来调用 REST/SOAP 端点,然后在映射实际响应后返回“Response”对象。

历史

您可以在 GitHub Zerocode README 文件中找到发布历史和更多关于该框架的信息。

当前版本是 1.3.x,请访问 Maven Central 上的 zerocode 获取最新版本。

开源库的动机

Zerocode 的诞生源于该国最大的数字化转型项目之一,由于以下原因,需要一种简单且易于维护的测试方法:

  • 大量微服务用于实现各种工作流
  • 大量数据管道用于将各种遗留系统的数据摄取到项目的大数据存储中。
  • 微服务需进行隔离测试和集成测试

经过多次头脑风暴会议,自动化测试人员和开发人员达成共识,编写测试时应

  • 允许任何人轻松更改、共享和维护它们
  • 避免编写样板粘性代码。
  • 大量的测试需要组织良好且业务可读
  • 本地笔记本电脑配置多个环境(localhost、ci/dev/dit、SIT、UAT、PRE-PROD 等),以便轻松地像 JUnit 测试一样触发测试。

会议的结果是考虑一种基于步骤的场景构建方法,其中底层测试和测试数据从引用的 JSON 文件中获取。

YAML/JSON 被大多数流行的 IDE 广泛支持,因此被证明是非常高效的方法。

希望为这个开源库做贡献?

这很容易!请先提出一个问题,然后遵循 CONTRIBUTING.md 中的指南。

谁在使用 Zerocode?

请访问 README 文件中的“谁在使用”部分。

© . All rights reserved.