1.1.17 • Published 3 years ago

zhl-api-test v1.1.17

Weekly downloads
11
License
ISC
Repository
-
Last release
3 years ago

功能

  • 尽量简单,不要有复杂的依赖;
  • HTTP API 测试;
  • 因为 API 之间有依赖关系,所以要能按照依赖关系进行执行;
  • 某些 API 需要从新的 Session 开始执行,需要满足可以设定 API 测试线从新的 Session 执行;
  • API 可以多次循环测试;
  • API 线可以多次循环测试;
  • 方便的开启 debug 功能;

使用方法

作者习惯使用 Spring Boot 搭建服务,故使用 Spring Boot 进行说明。

假设 Spring Boot 项目已经搭建好,现在需要开始 API 测试,目录结构如下:

spring-boot-project
    |---- ...
    |---- ...
    |---- ...

导入框架

打开 CMD,cd 到 spring-boot-project 工程目录下。

新建 node 项目,如 api-test

创建目录 api-test

mkdir api-test
cd api-test

npm 初始化 node 项目

npm init

然后根据提示填写信息,不清楚的话就一路回车。

添加依赖库

打开 api-test 目录下 package.json 文件,修改如下:

{
  "name": "api-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  // 添加这几行
  "dependencies": {
    "zhl-api-test": "^1.1.12"
  }
}

安装依赖

npm i

等待执行完成。

导入框架完成

此时目录结构:

spring-boot-project
    |---- ...
    |---- ...
    |---- ...
    |---- api-test
         |---- node_modules
              |---- ...
              |---- ...
              |---- ...
         |---- package.json
         |---- package-lock.json
    |---- ...

添加测试单元

在 api-test 目录中,添加 index.js 文件。文件内容如下:

const {StartApiTest} = require('zhl-api-test')

// 正确的请求测试
const TEST_CORRECT_REQUEST = {
    'Hello 测试': {
        dependentItem: [],
        requestConfig: {
            method: 'post',
            url: '/say/hello'
        },
        assert: (data) => {
            console.assert(null != data, "返回数据不为空");
        },
    },
}

// 错误的请求测试
const TEST_WRONG_REQUEST = {
}

const TEST_LIST = Object.assign({}, TEST_CORRECT_REQUEST, TEST_WRONG_REQUEST);

// 进行测试
StartApiTest(TEST_LIST);

注:也可在 api-test 中建目录,分模块进行测试。

创建自动化测试流程

在 Spring Boot 中,一般是在 src/test 目录中的 ...ApplicationTests 类中定义测试。

package ...;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

@SpringBootTest(classes = Application.class,
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ApplicationTests {

    public static final String NODE_ACTUATOR =
            "node 可执行文件位置,Windows 中为 node.exe,Linux 中为 node";
    private static final List<String> outputString = new CopyOnWriteArrayList<>();
    @LocalServerPort
    private int port;

    private void printMessage(final InputStream input) {
        new Thread(() -> {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
            String line;
            try {
                while ((line = bufferedReader.readLine()) != null) {
                    outputString.add(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

    @Test
    void apiTest() {
        System.out.println("port = " + port);
        try {
            File workDir = new File("./api-test");
            Process process = Runtime.getRuntime().exec(NODE_ACTUATOR + " index.js http://127.0.0.1:" + port, null, workDir);

            printMessage(process.getErrorStream());
            printMessage(process.getInputStream());
            int exitVal = process.waitFor();

            System.out.println("\n\n===============================================================================\n\n");
            System.out.println("node test finished, Exit code is " + exitVal);
            AtomicBoolean noErrorMessage = new AtomicBoolean(true);
            outputString.forEach(line -> {
                if (null == line) {
                    return;
                }
                String lineLowerCase = line.trim().toLowerCase();
                if (lineLowerCase.contains("error:")
                        || lineLowerCase.contains("assertion failed")) {
                    System.err.println(line);
                    if (noErrorMessage.get()) {
                        noErrorMessage.set(false);
                    }
                } else {
                    System.out.println(line);
                }
            });
            assert exitVal == 0;
            assert noErrorMessage.get();

            System.out.println("\n\n===============================================================================\n\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

此时,便完成了自动化测试。

测试单元规则详解

单个测试:

// 这是一条测试
const TestNodeTemplate = {
    // String,测试名称,可为空
    // 在测试组中作为键值定义即可
    name: '',
    
    // Array<String>,必须定义,所依赖的测试,比如,登录前,需要先请求得到验证码,那么登录测试就得依赖于请求验证码测试
    dependentItem: [],
    
    // Object or Function,必须定义,可以是静态的 Object,也可以定义函数,注:如是函数,则每次请求都会调用函数
    // requestConfig 具体内容参照 AxiosRequestConfig,必须要定义的有 url,method
    requestConfig: {},
    
    // Function or Array<Function>,可为空,数据通过函数或函数数组提供
    // 如果是函数数组,则 cycle 值会被设置为 data.length
    data: () => {
    },
    data: [() => {}, () => {}, () => {}, ....]
    
    // 'parallel' or 'serial',定义测试是并行执行还是串行执行。默认 'serial'
    parallelOrSerial: 'serial',
    
    // Function,必须定义,对返回内容数据的断言函数
    assert: (data, requestConfig) => {
        // 如:
        console.assert(null != data);
    },
    
    // Function,对返回数据的断言函数,默认空函数
    assertResponse: (response, requestConfig) => {
        // 如:
        console.assert(null != response);
    },
    
    // Integer,测试自身循环次数,默认为 1
    // 如果 data 是函数数组,则 cycle 值会被设置为 cycle × data.length
    cycles: 1,
    
    // 循环结束函数。默认为一个返回 true 的函数,
    // 如果该设置了该函数,则 cycle 的值会被重新设置,如果 data 是函数,则 cycle 设置为 1,如果是 函数数组,则设置为 data.length
    // 注:并行执行不支持 requestConfig 函数,且 response 的结果不可预期
    // 有些测试需要判断数据是否符合一定条件,才会结束循环
    loopEndFunction: (response, requestConfig) => {
        return true;
    }
    
    // 延迟执行,默认为 0,不延迟
    delay: 0,
    
    // Integer,测试线循环次数,默认为 1
    // 该定义必须定义在没有任何测试依赖的测试上(即测试线最末尾的测试)
    lineCycles: 1,
    
    // true or false,是否需要在新 Session 中进行该测试,默认 false
    newSession: false,
    
    // Function,回调函数,可做一些请求后的处理工作,默认空函数
    callback: (response, requestConfig) => {
    }
}

测试线:

const TEST_CORRECT_REQUEST = {
    // 定义单个测试,测试名作为键值
    'Hello 测试': {
        dependentItem: [],
        requestConfig: {
            method: 'post',
            url: '/say/hello'
        },
        assert: (data) => {
            console.assert(null != data, "返回数据不为空");
        },
    },
}

依赖规则

假设有如下测试依赖:

A 依赖 B
B 依赖 C、D、E
C 依赖 F
D、F 依赖 G
E 依赖 H

如图示:
A--->B--->C--->F--->G
     |--->D---------↑ 
     |
     |--->E-------->H

则有两条测试线,分别是以 G 和 H 开始的测试线,测试单元执行过程如下:

// 即 B 一定先于 A 执行,(C、F)组合与 D 可以并行执行,无执行顺序要求(这里展示的是串行执行过程,所以只是其中一种情况),
// 但 C、D 一定先于 B 执行,F 一定先于 C 执行,当然,最初执行肯定是 G
G --> D --> F --> C --> B --> A

// H 的这条线只需跟着依赖走即可
H --> E --> B --> A

原则就是:测试单元的依赖一定被全部执行完成后才会执行测试单元本身。

1.1.17

3 years ago

1.1.16

3 years ago

1.1.15

3 years ago

1.1.14

3 years ago

1.1.13

3 years ago

1.1.12

3 years ago

1.1.11

3 years ago

1.1.10

3 years ago

1.1.9

4 years ago

1.1.8

4 years ago

1.1.7

4 years ago

1.1.6

4 years ago

1.1.5

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago