51Testing软件测试论坛

标题: Apitest 接口自动化测试工具 [打印本页]

作者: lsekfe    时间: 2022-5-17 11:46
标题: Apitest 接口自动化测试工具
安装
  Apitest工具是单可执行文件,不需要安装,放到PATH路径下面就可以直接运行:
  1. # linux
  2.   curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-linux
  3.   chmod +x apitest
  4.   sudo mv apitest /usr/local/bin/
  5.   # macos
  6.   curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-macos
  7.   chmod +x apitest
  8.   sudo mv apitest /usr/local/bin/
  9.   # npm
  10.   npm install -g @sigodenjs/apitest
复制代码
开始使用
  编写测试文件 httpbin.jsona
  1.  {
  2.     test1: {
  3.       req: {
  4.         url: "https://httpbin.org/anything",
  5.         query: {
  6.           k1: "v1",
  7.         },
  8.       },
  9.       res: {
  10.         body: { @partial
  11.           args: {
  12.             "k1": "v2", // 注意,这儿应该是"v1", 我们故意写"v2"以测试Apitest的反应
  13.           },
  14.           url: "https://httpbin.org/anything?k1=v1",
  15.         }
  16.       }
  17.     }
  18.   }
复制代码
执行如下命令测试接口。

  1.  apitest httpbin.jsona
复制代码
其结果如下:

  1.  main
  2.     test1 (2.554) ?
  3.     main.test1.res.body.args.k1: v2 ≠ v1
  4.     {
  5.       "req": {
  6.         "url": "https://httpbin.org/anything",
  7.         "query": {
  8.           "k1": "v1"
  9.         }
  10.       },
  11.       "res": {
  12.         "headers": {
  13.           "date": "Thu, 17 Jun 2021 15:01:51 GMT",
  14.           "content-type": "application/json",
  15.           "content-length": "400",
  16.           "connection": "close",
  17.           "server": "gunicorn/19.9.0",
  18.           "access-control-allow-origin": "*",
  19.           "access-control-allow-credentials": "true"
  20.         },
  21.         "status": 200,
  22.         "body": {
  23.           "args": {
  24.             "k1": "v1"
  25.           },
  26.           "data": "",
  27.           "files": {},
  28.           "form": {},
  29.           "headers": {
  30.             "Accept": "application/json, text/plain, */*",
  31.             "Host": "httpbin.org",
  32.             "User-Agent": "axios/0.21.1",
  33.             "X-Amzn-Trace-Id": "Root=1-60cb63df-1b8592de3767882a6e865295"
  34.           },
  35.           "json": null,
  36.           "method": "GET",
  37.           "origin": "119.123.242.225",
  38.           "url": "https://httpbin.org/anything?k1=v1"
  39.         }
  40.       }
  41.     }
复制代码
Apitest 发现了k1的值异常 main.test1.res.body.args.k1: v2 ≠ v1 并打印错误,同时还打印了接口请求响应详情。
  如果我们修改 main.test1.res.body.args.k1 值 v2 => v1 后再执行测试。
  1.   apitest httpbin.jsona
复制代码
其结果如下:
  1. main
  2.     test1 (1.889)
复制代码
Apitest 报告测试通过了。
  原理
  Apitest 执行测试文件时会加载全部测试用例,逐一执行,其执行过程可以描述为:根据 req 部分构造请求发送给服务器,收到响应后依据 res 校验响应数据,然后打印结果。
  Apitest 中的用例文件格式是 JSONA。 JSONA是JSON的超集,减轻了一些JSON语法限制(不强制要求双引号,支持注释等),再添加了一个特性:注解。上面例子中的@partial就是注解。
  为什么使用JSONA?
  接口测试的本质的就是构造并发送req数据,接收并校验res数据。数据即是主体又是核心,而JSON是最可读最通用的数据描述格式。
  接口测试还需要某些特定逻辑。比如请求中构造随机数,在响应中只校验给出的部分数据。
  JSONA = JSON + Annotation(注解)。JSON负责数据部分,注解负责逻辑部分。完美的贴合接口测试需求。
  特性
  ·跨平台
  · DSL
   - 类JSON,没有学习难度
   - 编写简单,阅读容易
   - 不要求编写者会编程
  · 数据即断言
  · 数据可访问
  · 支持Mock
  · 支持Mixin
  · 支持CI
  · 支持TDD
  · 支持用户定义函数
  · 跳过,延时,重试和循环
  · 支持Form,文件上传,GraphQL
  示例
  全等校验
  默认请求下,Apitest 进行全等校验。
  · 简单类型数据(null,boolean,string,number)完全相等
  · object数据属性和属性值完全相等,字段顺序可以不一致
  · array数据元素长度和各元素完全相等,元素顺序也要一致
  1.  {
  2.     test1: { @client("echo")
  3.       req: {
  4.         any: null,
  5.         bool: true,
  6.         str: "string",
  7.         int: 3,
  8.         float: 0.3,
  9.         obj: {a:3, b:4},
  10.         arr: [3,4],
  11.       },
  12.       res: {
  13.         any: null,
  14.         bool: true,
  15.         str: "string",
  16.         int: 3,
  17.         float: 0.3,
  18.         obj: {a:3, b:4},
  19.         // obj: {b:4, b:3}, object类数据字段顺序可以不一致
  20.         arr: [3,4],
  21.       }
  22.     }
  23.   }
复制代码
 Apitest 保证:只有当实际接收到的 res 数据与我们用例中描述的 res 数据全等,测试才会通过。
  数组校验技巧
  Apitest 默认全等校验,而接口返回的array数据可能几十上百条,怎么办?
  通常接口数据是结构化的,我们可以只校验数组第一个元素。
  1.  {
  2.     test1: { @client("echo")
  3.       req: {
  4.         arr: [
  5.           {name: "v1"},
  6.           {name: "v2"},
  7.           {name: "v3"},
  8.         ]
  9.       },
  10.       res: {
  11.         arr: [ @partial
  12.           {
  13.             name: "", @type
  14.           }
  15.         ],
  16.       }
  17.     }
  18.   }
复制代码
如果array数据的长度也很关键呢?
  1. {
  2.     test1: { @client("echo")
  3.       req: {
  4.         arr: [
  5.           {name: "v1"},
  6.           {name: "v2"},
  7.           {name: "v3"},
  8.         ]
  9.       },
  10.       res: {
  11.         arr: [ @every
  12.           [ @partial
  13.               {
  14.                 name: "", @type
  15.               }
  16.           ],
  17.           `$.length === 3`, @eval
  18.         ],
  19.       }
  20.     }
  21.   }
复制代码
对象校验技巧
  Apitest 默认全等校验,而接口返回的object数据的属性很多,我们只关注其中部分属性?
  1. {
  2.     test1: { @client("echo")
  3.       req: {
  4.         obj: {
  5.           a: 3,
  6.           b: 4,
  7.           c: 5,
  8.         }
  9.       },
  10.       res: {
  11.         obj: { @partial
  12.           b: 4,
  13.         }
  14.       }
  15.     }
  16.   }
复制代码
查询字符串
  通过 req.query 传入QueryString
  1. {
  2.     test1: {
  3.       req: {
  4.         url: "https://httpbin.org/get",
  5.         query: {
  6.           k1: "v1",
  7.           k2: "v2",
  8.         }
  9.       },
  10.       res: {
  11.         body: { @partial
  12.           url: "https://httpbin.org/get?k1=v1&k2=v2",
  13.         }
  14.       }
  15.     }
  16.   }
复制代码
当然你可以把QueryString直接写在req.url中。
  1. {
  2.     test1: {
  3.       req: {
  4.         url: "https://httpbin.org/get?k1=v1&k2=v2",
  5.       },
  6.       res: {
  7.         body: { @partial
  8.           url: "https://httpbin.org/get?k1=v1&k2=v2",
  9.         }
  10.       }
  11.     }
  12.   }
复制代码
路径变量
  通过 req.params 传入路径变量。
  1. {
  2.     test1: {
  3.       req: {
  4.         url: "https://httpbin.org/anything/{id}",
  5.         params: {
  6.           id: 3,
  7.         }
  8.       },
  9.       res: {
  10.         body: { @partial
  11.           url: "https://httpbin.org/anything/3"
  12.         }
  13.       }
  14.     }
  15.   }
复制代码
请求头/响应头
  通过 req.headers 传入请求头,通过 res.headers 校验响应头。
  1. {
  2.     setCookies: { @describe("response with set-cookies header")
  3.       req: {
  4.         url: "https://httpbin.org/cookies/set",
  5.         query: {
  6.           k1: "v1",
  7.           k2: "v2",
  8.         },
  9.       },
  10.       res: {
  11.         status: 302,
  12.         headers: { @partial
  13.           'set-cookie': [
  14.             "k1=v1; Path=/",
  15.             "k2=v2; Path=/",
  16.           ],
  17.         },
  18.         body: "", @type
  19.       }
  20.     },
  21.     useCookies: { @describe("request with cookie header")
  22.       req: {
  23.         url: "https://httpbin.org/cookies",
  24.         headers: {
  25.           Cookie: `setCookies.res.headers["set-cookie"]`, @eval
  26.         }
  27.       },
  28.       res: {
  29.         body: { @partial
  30.           cookies: {
  31.             k1: "v1",
  32.             k2: "v2",
  33.           }
  34.         }
  35.       },
  36.     },
  37.   }
复制代码
用例数据变量导出与引用
  凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。
  Apitest 中可以使用 @eval 注解引用用例数据。
  比如上面例子中setCookies.res.headers["set-cookie"],就是引用前面setCookies用例的set-cookie响应头数据。
  表单: x-www-form-urlencoded
  1.  {
  2.     test1: { @describe('test form')
  3.       req: {
  4.         url: "https://httpbin.org/post",
  5.         method: "post",
  6.         headers: {
  7.           'content-type':"application/x-www-form-urlencoded"
  8.         },
  9.         body: {
  10.           v1: "bar1",
  11.           v2: "Bar2",
  12.         }
  13.       },
  14.       res: {
  15.         status: 200,
  16.         body: { @partial
  17.           form: {
  18.             v1: "bar1",
  19.             v2: "Bar2",
  20.           }
  21.         }
  22.       }
  23.     },
  24.   }
复制代码
表单: multipart/form-data
  结合 @file 注解实现文件上传。
  1.  {
  2.     test1: { @describe('test multi-part')
  3.       req: {
  4.         url: "https://httpbin.org/post",
  5.         method: "post",
  6.         headers: {
  7.           'content-type': "multipart/form-data",
  8.         },
  9.         body: {
  10.           v1: "bar1",
  11.           v2: "httpbin.jsona", @file
  12.         }
  13.       },
  14.       res: {
  15.         status: 200,
  16.         body: { @partial
  17.           form: {
  18.             v1: "bar1",
  19.             v2: "", @type
  20.           }
  21.         }
  22.       }
  23.     }
  24.   }
复制代码
GraphQL

  1. {
  2.     test1: { @describe("test graphql")
  3.       req: {
  4.         url: "https://api.spacex.land/graphql/",
  5.         body: {
  6.           query: `\`query {
  7.     launchesPast(limit: ${othertest.req.body.count}) {
  8.       mission_name
  9.       launch_date_local
  10.       launch_site {
  11.         site_name_long
  12.       }
  13.     }
  14.   }\`` @eval
  15.         }
  16.       },
  17.       res: {
  18.         body: {
  19.           data: {
  20.             launchesPast: [ @partial
  21.               {
  22.                 "mission_name": "", @type
  23.                 "launch_date_local": "", @type
  24.                 "launch_site": {
  25.                   "site_name_long": "", @type
  26.                 }
  27.               }
  28.             ]
  29.           }
  30.         }
  31.       }
  32.     }
  33.   }
复制代码
http(s)代理

  1.  {
  2.     @client({
  3.       name: "default",
  4.       type: "http",
  5.       options: {
  6.         proxy: "http://localhost:8080",
  7.       }
  8.     })
  9.     test1: {
  10.       req: {
  11.         url: "https://httpbin.org/ip",
  12.       },
  13.       res: {
  14.         body: {
  15.           origin: "", @type
  16.         }
  17.       }
  18.     }
  19.   }
复制代码
Apitest 支持通过 HTTP_PROXY HTTPS_PROXY 环境变量开全局代理。
  多个接口服务地址
  1. {
  2.     @client({
  3.       name: "api1",
  4.       type: "http",
  5.       options: {
  6.         baseURL: "http://localhost:3000/api/v1",
  7.       }
  8.     })
  9.     @client({
  10.       name: "api2",
  11.       type: "http",
  12.       options: {
  13.         baseURL: "http://localhost:3000/api/v2",
  14.       }
  15.     })
  16.     test1: { @client("api1")
  17.       req: {
  18.         url: "/signup", // => http://localhost:3000/api/v1/signup
  19.       }
  20.     },
  21.     test2: { @client("api2")
  22.       req: {
  23.         url: "/signup", // => http://localhost:3000/api/v2/signup
  24.       }
  25.     }
  26.   }
复制代码
自定义超时
  你可以设置客户端超时,影响所有使用该客户端的接口。
  1.  {
  2.     @client({
  3.       name: "default",
  4.       type: "http",
  5.       options: {
  6.         timeout: 30000,
  7.       }
  8.     })
  9.   }
复制代码
你也可以为某个用例设置超时。

  1.  {
  2.     test1: { @client({options:{timeout: 30000}})
  3.     }
  4.   }
复制代码
环境变量传递数据

  1. {
  2.     test1: {
  3.       req: {
  4.         headers: {
  5.           "x-key": "env.API_KEY", @eval
  6.         }
  7.       }
  8.     }
  9.   }
复制代码
mock数据

{
    login1: {
      req: {
        url: "/signup",
        body: {
          username: 'username(3)', @mock
          password: 'string(12)', @mock
          email: `req.username + "@gmail.com"`, @eval
        }
      }
    }
  }

 Apitest 支持近40个mock函数。下面列些常用的。

  1. {
  2.     test1: {
  3.       req: {
  4.         email: 'email', @mock
  5.         username: 'username', @mock
  6.         integer: 'integer(-5, 5)', @mock
  7.         image: 'image("200x100")', @mock
  8.         string: 'string("alpha", 5)', @mock
  9.         date: 'date', @mock  // iso8601格式的当前时间 // 2021-06-03T07:35:55Z
  10.         date2: 'date("","2 weeks ago")', @mock // 2周前
  11.         sentence: 'sentence', @mock
  12.         cnsentence: 'cnsentence', @mock // 中文段落   
  13.       }
  14.     }
  15.   }
复制代码
 用例组

{
    @describe("这是一个模块")
    @client({name:"default",kind:"echo"})
    group1: { @group @describe("这是一个组")
      test1: { @describe("最内用例")
        req: {
        }
      },
      group2: { @group @describe("这是一个嵌套组")
        test1: { @describe("嵌套组内的用例")
          req: {
          }
        }
      }
    }
  }

 上面的测试文件打印如下:

  1.  这是一个模块
  2.     这是一个组
  3.       最内用例
  4.       这是一个嵌套组
  5.         嵌套组内的用例
复制代码
 跳过用例(组)

 {
    test1: { @client("echo")
      req: {
      },
      run: {
        skip: `othertest.res.status === 200`, @eval
      }
    }
  }

 延时执行用例(组)

 {
    test1: { @client("echo")
      req: {
      },
      run: {
        delay: 1000, // 延时毫秒
      }
    }
  }

重试用例(组)

 {
    test1: { @client("echo")
      req: {
      },
      run: {
        retry: {
          stop:'$run.count> 2', @eval // 终止重试条件
          delay: 1000, // 重试间隔毫秒
        }
      },
    }
  }

重复执行用例(组)

 {
    test1: { @client("echo")
      req: {
        v1:'$run.index', @eval
        v2:'$run.item', @eval
      },
      run: {
        loop: {
          delay: 1000, // 重复执行间隔毫秒
          items: [  // 重复执行数据
            'a',
            'b',
            'c',
          ]
        }
      },
    }
  }

如果不在意数据,只想重复执行多少次的话,可以这样设置。

  1.  {
  2.     test1: {
  3.       run: {
  4.         delay: 1000,
  5.         items: `Array(5)`, @eval
  6.       }
  7.     }
  8.   }
复制代码
强制打印详情
  常规模式下,接口如果没有出错是不会打印数据详情的。通过设置run.dump为true强制打印详情数据。
  1. {
  2.     test1: { @client("echo")
  3.       req: {
  4.       },
  5.       run: {
  6.         dump: true,
  7.       }
  8.     }
  9.   }
复制代码
抽离公用逻辑以复用
  首先创建一个文件存储Mixin定义的文件。
  1. // mixin.jsona
  2.   {
  3.     createPost: { // 抽离路由信息到mixin
  4.       req: {
  5.         url: '/posts',
  6.         method: 'post',
  7.       },
  8.     },
  9.     auth1: { // 抽离鉴权到minxin
  10.       req: {
  11.         headers: {
  12.           authorization: `"Bearer " + test1.res.body.token`, @eval
  13.         }
  14.       }
  15.     }
  16.   }
复制代码
  1.  @mixin("mixin") // 引入 mixin.jsona 文件
  2.   {
  3.     createPost1: { @describe("写文章1") @mixin(["createPost", "auth1"])
  4.       req: {
  5.         body: {
  6.           title: "sentence", @mock
  7.         }
  8.       }
  9.     },
  10.     createPost2: { @describe("写文章2,带描述") @mixin(["createPost", "auth1"])
  11.       req: {
  12.         body: {
  13.           title: "sentence", @mock
  14.           description: "paragraph", @mock
  15.         }
  16.       }
  17.     },
  18.   }
复制代码
越是频繁用到的数据越适合抽离到Mixin。 
  自定义函数
  某些情况下,Apitest 内置的注解不够用,你可以使用自定义函数。
  编写函数lib.js
  1.  // 创建随机颜色
  2.   exports.makeColor = function () {
  3.     const letters = "0123456789ABCDEF";
  4.     let color = "#";
  5.     for (let i = 0; i < 6; i++) {
  6.       color += letters[Math.floor(Math.random() * 16)];
  7.     }
  8.     return color;
  9.   }
  10.   // 判断是否是ISO8601(2021-06-02:00:00.000Z)风格的时间字符串
  11.   exports.isDate = function (date) {
  12.     return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(date)
  13.   }
复制代码
使用函数
  1. @jslib("lib") // 引入js文件
  2.   {
  3.     test1: {
  4.       req: {
  5.         body: {
  6.           color: 'makeColor()', @eval // 调用 `makeColor` 函数生成随机颜色
  7.         }
  8.       },
  9.       res: {
  10.         body: {
  11.           createdAt: 'isDate($)', @eval // $ 表示须校验字段,对应响应数据`res.body.createdAt`
  12.           // 当然你可以直接使用regex
  13.           updatedAt: `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test($)`, @eval
  14.         }
  15.       }
  16.     }
  17.   }
复制代码






















欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2