Apitest 接口自动化测试工具

发表于:2022-5-17 09:16

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:sigo    来源:稀土掘金

  安装
  Apitest工具是单可执行文件,不需要安装,放到PATH路径下面就可以直接运行:
  # linux
  curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-linux 
  chmod +x apitest
  sudo mv apitest /usr/local/bin/
  # macos
  curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-macos
  chmod +x apitest
  sudo mv apitest /usr/local/bin/
  # npm
  npm install -g @sigodenjs/apitest

  开始使用
  编写测试文件 httpbin.jsona
  {
    test1: {
      req: {
        url: "https://httpbin.org/anything",
        query: {
          k1: "v1",
        },
      },
      res: {
        body: { @partial
          args: {
            "k1": "v2", // 注意,这儿应该是"v1", 我们故意写"v2"以测试Apitest的反应
          },
          url: "https://httpbin.org/anything?k1=v1",
        }
      }
    }
  }

  执行如下命令测试接口。
  apitest httpbin.jsona

  其结果如下:
  main
    test1 (2.554) ?
    main.test1.res.body.args.k1: v2 ≠ v1
    {
      "req": {
        "url": "https://httpbin.org/anything",
        "query": {
          "k1": "v1"
        }
      },
      "res": {
        "headers": {
          "date": "Thu, 17 Jun 2021 15:01:51 GMT",
          "content-type": "application/json",
          "content-length": "400",
          "connection": "close",
          "server": "gunicorn/19.9.0",
          "access-control-allow-origin": "*",
          "access-control-allow-credentials": "true"
        },
        "status": 200,
        "body": {
          "args": {
            "k1": "v1"
          },
          "data": "",
          "files": {},
          "form": {},
          "headers": {
            "Accept": "application/json, text/plain, */*",
            "Host": "httpbin.org",
            "User-Agent": "axios/0.21.1",
            "X-Amzn-Trace-Id": "Root=1-60cb63df-1b8592de3767882a6e865295"
          },
          "json": null,
          "method": "GET",
          "origin": "119.123.242.225",
          "url": "https://httpbin.org/anything?k1=v1"
        }
      }
    }

  Apitest 发现了k1的值异常 main.test1.res.body.args.k1: v2 ≠ v1 并打印错误,同时还打印了接口请求响应详情。
  如果我们修改 main.test1.res.body.args.k1 值 v2 => v1 后再执行测试。
  apitest httpbin.jsona

  其结果如下:
  main
    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数据元素长度和各元素完全相等,元素顺序也要一致
  {
    test1: { @client("echo")
      req: {
        any: null,
        bool: true,
        str: "string",
        int: 3,
        float: 0.3,
        obj: {a:3, b:4},
        arr: [3,4],
      },
      res: {
        any: null,
        bool: true,
        str: "string",
        int: 3,
        float: 0.3,
        obj: {a:3, b:4},
        // obj: {b:4, b:3}, object类数据字段顺序可以不一致
        arr: [3,4],
      }
    }
  }

  Apitest 保证:只有当实际接收到的 res 数据与我们用例中描述的 res 数据全等,测试才会通过。
  数组校验技巧
  Apitest 默认全等校验,而接口返回的array数据可能几十上百条,怎么办?
  通常接口数据是结构化的,我们可以只校验数组第一个元素。
  {
    test1: { @client("echo")
      req: {
        arr: [
          {name: "v1"},
          {name: "v2"},
          {name: "v3"},
        ]
      },
      res: {
        arr: [ @partial
          {
            name: "", @type
          }
        ],
      }
    }
  }

  如果array数据的长度也很关键呢?
  {
    test1: { @client("echo")
      req: {
        arr: [
          {name: "v1"},
          {name: "v2"},
          {name: "v3"},
        ]
      },
      res: {
        arr: [ @every
          [ @partial
              {
                name: "", @type
              }
          ],
          `$.length === 3`, @eval
        ],
      }
    }
  }

  对象校验技巧
  Apitest 默认全等校验,而接口返回的object数据的属性很多,我们只关注其中部分属性?
  {
    test1: { @client("echo")
      req: {
        obj: {
          a: 3,
          b: 4,
          c: 5,
        }
      },
      res: {
        obj: { @partial
          b: 4,
        }
      }
    }
  }

  查询字符串
  通过 req.query 传入QueryString
  {
    test1: {
      req: {
        url: "https://httpbin.org/get",
        query: {
          k1: "v1",
          k2: "v2",
        }
      },
      res: {
        body: { @partial
          url: "https://httpbin.org/get?k1=v1&k2=v2",
        }
      }
    }
  }

  当然你可以把QueryString直接写在req.url中。
  {
    test1: {
      req: {
        url: "https://httpbin.org/get?k1=v1&k2=v2",
      },
      res: {
        body: { @partial
          url: "https://httpbin.org/get?k1=v1&k2=v2",
        }
      }
    }
  }

  路径变量
  通过 req.params 传入路径变量。
  {
    test1: {
      req: {
        url: "https://httpbin.org/anything/{id}",
        params: {
          id: 3,
        }
      },
      res: {
        body: { @partial
          url: "https://httpbin.org/anything/3"
        }
      }
    }
  }

  请求头/响应头
  通过 req.headers 传入请求头,通过 res.headers 校验响应头。
  {
    setCookies: { @describe("response with set-cookies header")
      req: {
        url: "https://httpbin.org/cookies/set",
        query: {
          k1: "v1",
          k2: "v2",
        },
      },
      res: {
        status: 302,
        headers: { @partial
          'set-cookie': [
            "k1=v1; Path=/",
            "k2=v2; Path=/",
          ], 
        },
        body: "", @type
      }
    },
    useCookies: { @describe("request with cookie header")
      req: {
        url: "https://httpbin.org/cookies",
        headers: {
          Cookie: `setCookies.res.headers["set-cookie"]`, @eval
        }
      },
      res: {
        body: { @partial
          cookies: {
            k1: "v1",
            k2: "v2",
          }
        }
      },
    },
  }

  用例数据变量导出与引用
  凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。
  Apitest 中可以使用 @eval 注解引用用例数据。
  比如上面例子中setCookies.res.headers["set-cookie"],就是引用前面setCookies用例的set-cookie响应头数据。
  表单: x-www-form-urlencoded
  {
    test1: { @describe('test form')
      req: {
        url: "https://httpbin.org/post",
        method: "post",
        headers: {
          'content-type':"application/x-www-form-urlencoded"
        },
        body: {
          v1: "bar1",
          v2: "Bar2",
        }
      },
      res: {
        status: 200,
        body: { @partial
          form: {
            v1: "bar1",
            v2: "Bar2",
          }
        }
      }
    },
  }

  表单: multipart/form-data
  结合 @file 注解实现文件上传。
  {
    test1: { @describe('test multi-part')
      req: {
        url: "https://httpbin.org/post",
        method: "post",
        headers: {
          'content-type': "multipart/form-data",
        },
        body: {
          v1: "bar1",
          v2: "httpbin.jsona", @file
        }
      },
      res: {
        status: 200,
        body: { @partial
          form: {
            v1: "bar1",
            v2: "", @type
          }
        }
      }
    }
  }

  GraphQL
  {
    test1: { @describe("test graphql")
      req: {
        url: "https://api.spacex.land/graphql/",
        body: {
          query: `\`query {
    launchesPast(limit: ${othertest.req.body.count}) {
      mission_name
      launch_date_local
      launch_site {
        site_name_long
      }
    }
  }\`` @eval
        }
      },
      res: {
        body: {
          data: {
            launchesPast: [ @partial
              {
                "mission_name": "", @type
                "launch_date_local": "", @type
                "launch_site": {
                  "site_name_long": "", @type
                }
              }
            ]
          }
        }
      }
    }
  }

  http(s)代理
  {
    @client({
      name: "default",
      type: "http",
      options: {
        proxy: "http://localhost:8080",
      }
    })
    test1: {
      req: {
        url: "https://httpbin.org/ip",
      },
      res: {
        body: {
          origin: "", @type
        }
      }
    }
  }

  Apitest 支持通过 HTTP_PROXY HTTPS_PROXY 环境变量开全局代理。
  多个接口服务地址
  {
    @client({
      name: "api1",
      type: "http",
      options: {
        baseURL: "http://localhost:3000/api/v1",
      }
    })
    @client({
      name: "api2",
      type: "http",
      options: {
        baseURL: "http://localhost:3000/api/v2",
      }
    })
    test1: { @client("api1")
      req: {
        url: "/signup", // => http://localhost:3000/api/v1/signup
      }
    },
    test2: { @client("api2")
      req: {
        url: "/signup", // => http://localhost:3000/api/v2/signup
      }
    }
  }

  自定义超时
  你可以设置客户端超时,影响所有使用该客户端的接口。
  {
    @client({
      name: "default",
      type: "http",
      options: {
        timeout: 30000, 
      }
    })
  }

  你也可以为某个用例设置超时。
  {
    test1: { @client({options:{timeout: 30000}})
    }
  }

  环境变量传递数据
  {
    test1: {
      req: {
        headers: {
          "x-key": "env.API_KEY", @eval
        }
      }
    }
  }

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

  Apitest 支持近40个mock函数。下面列些常用的。
  {
    test1: {
      req: {
        email: 'email', @mock
        username: 'username', @mock
        integer: 'integer(-5, 5)', @mock
        image: 'image("200x100")', @mock
        string: 'string("alpha", 5)', @mock
        date: 'date', @mock  // iso8601格式的当前时间 // 2021-06-03T07:35:55Z
        date2: 'date("","2 weeks ago")', @mock // 2周前
        sentence: 'sentence', @mock
        cnsentence: 'cnsentence', @mock // 中文段落    
      }
    }
  }

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

  上面的测试文件打印如下:
  这是一个模块
    这是一个组
      最内用例 
      这是一个嵌套组
        嵌套组内的用例 

  跳过用例(组)
  {
    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',
          ]
        }
      },
    }
  }

  如果不在意数据,只想重复执行多少次的话,可以这样设置。
  {
    test1: {
      run: {
        delay: 1000,
        items: `Array(5)`, @eval
      }
    }
  }

  强制打印详情
  常规模式下,接口如果没有出错是不会打印数据详情的。通过设置run.dump为true强制打印详情数据。
  {
    test1: { @client("echo")
      req: {
      },
      run: {
        dump: true,
      }
    }
  }

  抽离公用逻辑以复用
  首先创建一个文件存储Mixin定义的文件。
  // mixin.jsona
  {
    createPost: { // 抽离路由信息到mixin
      req: {
        url: '/posts',
        method: 'post',
      },
    },
    auth1: { // 抽离鉴权到minxin
      req: {
        headers: {
          authorization: `"Bearer " + test1.res.body.token`, @eval
        }
      }
    }
  }

  @mixin("mixin") // 引入 mixin.jsona 文件
  {
    createPost1: { @describe("写文章1") @mixin(["createPost", "auth1"])
      req: {
        body: {
          title: "sentence", @mock
        }
      }
    },
    createPost2: { @describe("写文章2,带描述") @mixin(["createPost", "auth1"])
      req: {
        body: {
          title: "sentence", @mock
          description: "paragraph", @mock
        }
      }
    },
  }

  越是频繁用到的数据越适合抽离到Mixin。 
  自定义函数
  某些情况下,Apitest 内置的注解不够用,你可以使用自定义函数。
  编写函数lib.js
  // 创建随机颜色
  exports.makeColor = function () {
    const letters = "0123456789ABCDEF";
    let color = "#";
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
  }
  // 判断是否是ISO8601(2021-06-02:00:00.000Z)风格的时间字符串
  exports.isDate = function (date) {
    return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(date)
  }

  使用函数
  @jslib("lib") // 引入js文件
  {
    test1: {
      req: {
        body: {
          color: 'makeColor()', @eval // 调用 `makeColor` 函数生成随机颜色
        }
      },
      res: {
        body: {
          createdAt: 'isDate($)', @eval // $ 表示须校验字段,对应响应数据`res.body.createdAt`
          // 当然你可以直接使用regex
          updatedAt: `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test($)`, @eval
        }
      }
    }
  }


  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号