跳至主要內容

使用 RestAssured 进行 API 测试

JavaTesting

使用 RestAssured 进行 API 测试

前言

本文将借助 RestAssured 工具,向大家介绍如何进行 API 测试,从而在团队中开启接口自动化之路。

本文的示例代码使用的是 Java 语言。尽管本文的首要读者是 Java 研发人员,但道理是相通的,其他语言的研发人员也能从中受益。

What

什么是 API 测试?简单来说,可以认为是针对 Controller 层的测试,但不是 Mock,而是会真实地处理请求,与数据库或外部服务进行交互。

Why

为什么要做 API 测试呢?

考虑有过这样的场景:

  • 加一个新功能,自测没问题,结果被测试人员发现一个旧模块出了问题,感到措手不及
  • 后端写好了接口,前端还没开发好界面,于是感觉不方便自测,因为没有界面,只好催前端快去做页面

API 测试就是来解决上述问题的。做 API 测试的原因有:

  • 必要性:做回归测试,避免添加新功能时破坏旧功能。
  • 便利性:方便本地调试,不用部署到线上,依赖界面去测试。
  • 资产化:让测试用例变成资产,与团队共享。

当然,要做好 API 测试,还要接受这样的认知: 接口自动化测试并不仅仅是测试人员事情,研发人员也有责任把它做好。 否则,研发人员难免会觉得这不关我的事, 从而不愿意写这种代码。 建议研发人员从以下方便思考其好处,提升行动的积极性:

  • 减少阻塞,接口自测不再依赖前端
  • 提高效率,本地就能自测,不用把应用部署到线上环境
  • 提高质量,减少部署到研发环境、前端一调用接口就 500 的情况

为什么不用Postman

Postman 确实是符合直觉的接口调试的第一选项。 但注意,调试不等于测试。

Postman 在实践过程中,最大的问题在于,无法将测试用例有效地资产化:

  • 你会在 Postman 里写断言吗?很少吧,你其实是在用肉眼去检查接口成功与否,这本质还是手工测试
  • 你的 Postman 数据能与团队共享吗?不能吧,大多数人的 Postman 数据是在本地的,也不会去付费创建一个团队以共享数据
  • 你的 Postman 数据在有版本管理吗?没有吧,大多数人的 Postman 数据是与源代码分离的,不利于维护与管理

另外,如果要与 CI 结合,Postman 的数据更适合使用 Node.js 的 Newmanopen in new window

考虑源代码是 Java,使用 RestAssured,编写 API 测试代码用同一种语言,可以减少使用者的心智负担较轻;并且与源代码放在同一个 Git 仓库中,易于管理。

因此,我仍然会使用 Postman,但更多是把它应用在出现线上问题时,直接复制一个 cURL 用来复现、排查问题的情况。

安装

下面将介绍如何用 Maven 安装 RestAssured。

复制以下内容到 pom.xml 即可。

    <!-- RestAssured for api testing -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <artifactId>json-path</artifactId>
                <groupId>io.rest-assured</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>json-path</artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>xml-path</artifactId>
        <version>5.3.0</version>
        <scope>test</scope>
    </dependency>

安装完成后,重启 Spring 容器。

如果安装依赖不成功,可以进行以下检查:

  • 显式指定 json-path 与 xml-path 的版本,并排除其他测试包(如 sping-boot-starter-test) 对 json-path 的依赖
  • 声明放在 JUnit 前面

快速上手

语法结构为: given()、when()、then()

given()  // 设置请求信息
        .log().body() // 输出请求日志
        .when()
        .get() // 发送请求
        .then()
        .log().body() // 输出响应日志
        .statusCode(200) // 断言响应
        ;

通用设置

以下代码可直接复制到 Java 测试类中。

private RequestSpecification requestSpec;

// @BeforeEach // JUnit5 
@Before // JUnit4
public void init() {
    String env = System.getenv("GITLAB_CI");
    if("true".equals(env)) {
        // Running in GitLab CI
        RestAssured.baseURI = "http://dev-domain.company.com";
    } else {
        // 本地调试 
        RestAssured.baseURI = "http://localhost:9402";
    }

    // 设置请求头
    RequestSpecBuilder builder = new RequestSpecBuilder();
    String token = getToken();
    builder.addHeader("Authorization", "bearer " + token); // jwt
    builder.addHeader("Content-Type", "application/json;charset=UTF-8");
    // 在 give().spec() 中使用即可
    requestSpec = builder.build();
    }

getToken方法的示例代码:

public static String getToken() {
    //return System.getenv("TOKEN");
    Map<String, String> params = new HashMap<>();
    params.put("username", "");
    params.put("password", "");
    return given()
            .spec(new RequestSpecBuilder().addHeader("Content-Type", "application/json;charset=UTF-8").build())
            .body(JSONObject.toJSONString(params))
        .when()
            .post("/api/v1/login")
        .then()
            .statusCode(200)
            .extract().path("payload.access_token");
}

请求示例

下面是一个 GET 请求示例,根据响应体进行断言:

@Test
public void getUser() {
  Map<String, Object> payload = given()
        .spec(requestSpec)
        .log().all()
        .when()
        .get("/v1/user/1386156540454400")
        .then()
        .log().body()
        .statusCode(200)
        .extract().path("payload");

  assertEquals(true, payload.get("isMain"));
}

下面是一个更完整的POST示例,包含了:

  • 组装数据
  • 设置body
  • 设置query
  • 判断响应体的数据结构
@Test
public void test(){
  Workflow workflow = new Workflow();
  workflow.setWorkflowId(1643167159934930966L);
  workflow.setWorkflowName("flow");
  List<Workflow> body = new ArrayList<>();
  body.add(workflow);

  given()
    .spec(requestSpec)
    .queryParam("query","value")
    .body(JSON.toJSONString(body))
    .log().body()
    .when()
    .post("/api/v1/your-api?t=1")
    .then()
    .log().body()
    .statusCode(200)
    .assertThat().body("code",org.hamcrest.Matchers.equalTo("0"))
    ;
}

提醒,在运行测试代码前,需要做两件事:

  • 一定保证 Web 服务已启动,因为这不是 Mock,而是会发送真实的请求。
  • 正确配置了环境变量 TOKEN。如果使用 IDEA,可以编辑运行配置,在环境变量里注入类似代码:TOKEN=Bearer xxx

接口依赖

有时在请求接口 B 之前,需要请求接口 A,于是就产生了接口依赖:B 依赖了 A。

此时可以使用 extract() 及 path() 获取请求 A 返回的数据。

  @Test
public void test(){
    // 发送第一个请求
    List<Map<String, String>>workflowList = getWorkflowList();
    if (workflowList.isEmpty()) {
      System.out.println("workflowList  empty, test not execute");
      return;
    }

    // 返回的数据结构是个 Map
    // 一般而言 Map<String, Object> 最通用,布尔值、数字会自动转换。
    // 不过这里接口返回的全是字符串,所以就 Map<String, String> 了
    Map<String, String> target = workflowList.get(0);

    WorkflowRunVO workflow = new WorkflowRunVO();
    workflow.setWorkflowId(Long.valueOf(target.get("workflowId")));
    workflow.setWorkflowName(target.get("name"));
    List<WorkflowRunVO> body = new ArrayList<>();
    body.add(workflow);

    // 在第二个请求中断言
    given()
    .spec(requestSpec)
    .body(JSON.toJSONString(body))
    .log().body()
    .when()
    .post("/api/v1/workflows")
    .then()
    .statusCode(200)
    .assertThat().body("code",equalTo("0")) // org.hamcrest.Matchers.equalTo
    .log().body();
}

private List<Map<String, String>>getWorkflowList(){
    return given()
    .spec(requestSpec)
    .when()
    .get("/api/v1/workflows")
    .then()
    .statusCode(200)
    .extract()
    .path("payload.content");
}

上传示例

RestAssured 很强大,还能处理上传与下载的请求,简直让人“爱了爱了”。 下面是具体的示例:

  @Test
public void upload(){
    // 需要本地有文件
    File file = new File("src/test/fixtures/txt-success");

    getImportResp(file)
    .assertThat().body("code",org.hamcrest.Matchers.equalTo("0"))
    .assertThat().body("payload",equalTo(true))
    ;
}

private ValidatableResponse getImportResp(File file){
    return given()
    .spec(requestSpec)
    .multiPart(file)
    .when()
    .post("/api/v1/upload")
    .then()
    .statusCode(200);
}

如果想在传文件的基础上,还传其他参数,可以这样写:

private ValidatableResponse getImportResp(File file) {
    return given()
    .spec(requestSpec)
    .multiPart("file", file, "application/json")
    .multiPart("extraParam", "value")
    .when()
    .post("/v1/upload")
    .then()
    .statusCode(200);
}

对应的前端请求代码为(记录一下,以备不时之需😃):

import axios from 'axios';

function getImportResp(file) {
  const formData = new FormData();
  formData.append('file', file, 'application/json');
  formData.append('extraParam', 'value');

  return axios.post('/v1/upload', formData)
    .then(response => {
      return response;
    })
    .catch(error => {
      throw error;
    });
}

下载示例

  @Test
public void download(){
    Map<String, Object> license = getLicenseList().get(0);
    if(Objects.isNull(license))return;

    // 因为设置的请求头跟默认的不一样,所以单独设置
    RequestSpecBuilder builder = new RequestSpecBuilder();
    String token=System.getenv("TOKEN");
    builder.addQueryParam("token",token.replace("Bearer ",""));
    builder.addHeader("Content-Type","application/json;charset=UTF-8");
    requestSpec=builder.build();

    String result = given()
    .spec(requestSpec)
    .log().body()
    .when()
    .get("/api/v1/download/"+license.get("id"))
    .then()
    .statusCode(200)
    .extract()
    .response()
    .asString() // 获取输出流打印的字符串
    ;

    System.out.println(result);
    Assert.assertEquals(5,result.split("\n").length);
}

看到全部用例都执行成功,非常爽快!
resetassured-download

持续集成

以集成 Gitlab CI 为例,其核心思路就是在 CI 环境运行 mvn test

具体做法可以参考笔者的Gitlab CI文章

其他问题

为什么不用 Pytest

如果编码代码的人员是测试人员,那可能首选 Pytest。但本文面向的读者的 Java 研发——既写 API,也写相应的测试代码。故选型理由参考前面 为什么不用Postman 的回答。

这也是单元测试吗

不是。运行上述测试代码,如果是测试本地接口,需要先在本地启动 Spring 容器;如果是测试线上接口,则需要先把应用部署到线上。因此,这是集成测试。

参考资料

官方文档:https://github.com/rest-assured/rest-assured/wiki/Usageopen in new window

上次编辑于:
贡献者: levy