接口测试 Seldom 2.0 - 让接口自动化测试更简单

虫师 · 2021年06月15日 · 最后由 文若 回复于 2023年08月17日 · 28878 次阅读
本帖已被设为精华帖!

前言

HTTP 接口测试很简单,不管工具、框架、还是平台,只要很的好的几个点就是好工具。

  1. 测试数据问题:比如删除接口,重复执行还能保持结果一致,必定要做数据初始化。
  2. 接口依赖问题:B 接口依赖 A 的返回值,C 接口依赖 B 接口的返回值。
  3. 加密问题:不同的接口加密规则不一样。有些用到时间戳、md5、base64、AES,如何提供种能力。
  4. 断言问题:有些接口返回的结构体很复杂,如何灵活的做到断言。

对于以上问题,工具和平台要么不支持,要么很麻烦,然而框架是最灵活的。

unittest/pytest + requests/https 直接上手写代码就好了,既简单又灵活。

那么同样是写代码,A 框架需要 10 行,B 框架只需要 5 行,然而又不失灵活性,那我当然是选择更少的了,毕竟,人生苦短嘛。

seldom 适合个人接口自动化项目,它有以下优势。

  • 可以写更少的代码
  • 自动生成 HTML/XML 测试报告
  • 支持参数化,减少重复的代码
  • 支持生成随机数据
  • 支持 har 文件转 case
  • 支持数据库操作

这些是 seldom 支持的功能,我们只需要集成 HTTP 接口库,并提供强大的断言即可。seldom 2.0 加入了 HTTP 接口自动化测试支持。

Seldom 兼容 Requests API 如下:

seldom requests
self.get() requests.get()
self.post() requests.post()
self.put() requests.put()
self.delete() requests.delete()

Seldom VS Request+unittest

先来看看 unittest + requests 是如何来做接口自动化的:

import unittest
import requests


class TestAPI(unittest.TestCase):

    def test_get_method(self):
        payload = {'key1': 'value1', 'key2': 'value2'}
        r = requests.get("http://httpbin.org/get", params=payload)
        self.assertEqual(r.status_code, 200)


if __name__ == '__main__':
    unittest.main()

这其实已经非常简洁了。同样的用例,用 seldom 实现。

# test_req.py
import seldom


class TestAPI(seldom.TestCase):

    def test_get_method(self):
        payload = {'key1': 'value1', 'key2': 'value2'}
        self.get("http://httpbin.org/get", params=payload)
        self.assertStatusCode(200)


if __name__ == '__main__':
    seldom.main()

主要简化点在,接口的返回数据的处理。当然,seldom 真正的优势在断言、日志和报告。

har to case

对于不熟悉 Requests 库的人来说,通过 Seldom 来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

首先,打开 fiddler 工具进行抓包,选中某一个请求。

然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

选择导出的文件格式。

点击next 保存为demo.har 文件。

最后,通过seldom -h2c 转为demo.py 脚本文件。

> seldom -h2c .\demo.har
.\demo.py
2021-06-14 18:05:50 [INFO] Start to generate testcase.
2021-06-14 18:05:50 [INFO] created file: D:\.\demo.py

demo.py 文件。

import seldom


class TestRequest(seldom.TestCase):

    def start(self):
        self.url = "http://httpbin.org/post"

    def test_case(self):
        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org", "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json", "Cookie": "lang=zh"}
        cookies = {"lang": "zh"}
        self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
        self.assertStatusCode(200)


if __name__ == '__main__':
    seldom.main()

运行测试

打开 debug 模式seldom.run(debug=True) 运行上面的用例。

> python .\test_req.py
2021-04-29 18:19:39 [INFO] A run the test in debug mode without generating HTML report!
2021-04-29 18:19:39 [INFO]
              __    __
   ________  / /___/ /___  ____ ____
  / ___/ _ \/ / __  / __ \/ __ ` ___/
 (__  )  __/ / /_/ / /_/ / / / / / /
/____/\___/_/\__,_/\____/_/ /_/ /_/
-----------------------------------------
                             @itest.info

test_get_method (test_req.TestAPI) ...
----------- Request 🚀 ---------------
url: http://httpbin.org/get         method: GET
----------- Response 🛬️ -------------
type: json
{'args': {'key1': 'value1', 'key2': 'value2'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.22.0', 'X-Amzn-Trace-Id': 'Root=1-608a883c-7b355ba81fcd0d287566405a'}, 'origin': '183.178.27.36', 'url': 'http://httpbin.org/get?key1=value1&key2=value2'}
ok

----------------------------------------------------------------------
Ran 1 test in 0.619s

OK

通过日志/报告都可以清楚的看到。

  • 请求的方法
  • 请求 url
  • 响应的类型
  • 响应的数据

更强大的断言

断言接口返回的数据是我们在做接口自动化很重要的工作。

assertJSON

接口返回结果如下:

{
  "args": {
    "hobby": [
      "basketball",
      "swim"
    ],
    "name": "tom"
  }
}

我的目标是断言namehobby 部分的内容。seldom 可以针对JSON文件进行断言。

import seldom


class TestAPI(seldom.TestCase):

    def test_assert_json(self):
        payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
        self.get("http://httpbin.org/get", params=payload)
        assert_json = {'args': {'hobby': ['swim', 'basketball'], 'name': 'tom'}}
        self.assertJSON(assert_json)

运行日志

test_get_method (test_req.TestAPI) ...
----------- Request 🚀 ---------------
url: http://httpbin.org/get         method: GET
----------- Response 🛬️ -------------
type: json
{'args': {'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.22.0', 'X-Amzn-Trace-Id': 'Root=1-608a896d-48fac4f6139912ba01d2626f'}, 'origin': '183.178.27.36', 'url': 'http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim'}
💡 Assert data has not key: headers
💡 Assert data has not key: origin
💡 Assert data has not key: url
ok

----------------------------------------------------------------------
Ran 1 test in 1.305s

OK

seldom 还会提示你还有哪些字段没有断言。

assertPath

接口返回数据如下:

{
  "args": {
    "hobby": 
      ["basketball", "swim"], 
    "name": "tom"
  }
}

seldom 中可以通过 path 进行断言:

import seldom


class TestAPI(seldom.TestCase):

    def test_assert_path(self):
        payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
        self.get("http://httpbin.org/get", params=payload)
        self.assertPath("name", "tom")
        self.assertPath("args.hobby[0]", "basketball")

assertSchema

有时并不关心数据本身是什么,而是需要断言数据的类型。 assertSchema 是基于 jsonschema 实现的断言方法。

jsonschema: https://json-schema.org/learn/

接口返回数据如下:

{
  "args": {
    "hobby": 
      ["basketball", "swim"], 
    "name": "tom", 
    "age": "18"
  }
}

seldom 中可以通过利用jsonschema 进行断言:

import seldom


class TestAPI(seldom.TestCase):

    def test_assert_schema(self):
        payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
        self.get("/get", params=payload)
        schema = {
            "type": "object",
            "properties": {
                "args": {
                    "type": "object",
                    "properties": {
                        "age": {"type": "string"},
                        "name": {"type": "string"},
                        "hobby": {
                            "type": "array",
                            "items": {
                                "type": "string"
                            },
                        }
                    }
                }
            },
        }
        self.assertSchema(schema)

是否再次感受到了 seldom 提供的断言非常灵活,强大。

接口数据依赖

在场景测试中,我们需要利用上一个接口的数据,调用下一个接口。

import seldom

class TestRespData(seldom.TestCase):

    def test_data_dependency(self):
        """
        Test for interface data dependencies
        """
        headers = {"X-Account-Fullname": "bugmaster"}
        self.get("/get", headers=headers)
        self.assertStatusCode(200)

        username = self.response["headers"]["X-Account-Fullname"]
        self.post("/post", data={'username': username})
        self.assertStatusCode(200)

seldom 提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

数据驱动

seldom 本来就提供的有强大的数据驱动,拿来做接口测试非常方便。

@data

import seldom
from seldom import data


class TestDDT(seldom.TestCase):

    @data([
        ("key1", 'value1'),
        ("key2", 'value2'),
        ("key3", 'value3')
    ])
    def test_data(self, key, value):
        """
        Data-Driver Tests
        """
        payload = {key: value}
        self.post("/post", data=payload)
        self.assertStatusCode(200)
        self.assertEqual(self.response["form"][key], value)

@file_data

创建data.json数据文件

{
 "login":  [
    ["admin", "admin123"],
    ["guest", "guest123"]
 ]
}

通过file_data实现数据驱动。

import seldom
from seldom import file_data


class TestDDT(seldom.TestCase):

    @file_data("data.json", key="login")
    def test_data(self, username, password):
        """
        Data-Driver Tests
        """
        payload = {username: password}
        self.post("http://httpbin.org/post", data=payload)
        self.assertStatusCode(200)
        self.assertEqual(self.response["form"][username], password)

更过数据文件 (csv/excel/yaml),参考

随机生成测试数据

seldom 提供随机生成测试数据方法,可以生成一些常用的数据。

import seldom
from seldom import testdata


class TestAPI(seldom.TestCase):

    def test_data(self):
        phone = testdata.get_phone()
        payload = {'phone': phone}
        self.get("http://httpbin.org/get", params=payload)
        self.assertPath("args.phone", phone)

更过类型的测试数据,参考

数据库操作

seldom 支持 sqlite3、MySQL 数据库操作。

sqlite3 MySQL
delete_data() delete_data()
insert_data() insert_data()
select_data() select_data()
update_data() update_data()
init_table() init_table()
close() close()

连接数据库

连接 sqlit3 数据库

from seldom.db_operation import SQLiteDB

db = SQLiteDB(r"D:\learnAPI\db.sqlite3")

连接 MySQL 数据库(需要)

  1. 安装 pymysql 驱动
> pip install pymysql
  1. 链接
from seldom.db_operation import MySQLDB

db = MySQLDB(host="127.0.0.1", 
             port="3306", 
             user="root", 
             password="123", 
             database="db_name")

操作方法

  • delete_data

删除表数据。

db.delete_data(table="user", where={"id":1})
  • insert_data

插入一条数据。

data = {'id': 1, 'username': 'admin', 'password': "123"},
db.insert_data(table="user", data=data)
  • select_data

查询表数据。

result = db.select_data(table="user", where={"id":1, "name": "tom"})
print(result)
  • update_data

更新表数据。

db.update_data(table="user", data={"name":"new tom"}, where={"name": "tom"})
  • init_table

批量插入数据,在插入之前先清空表数据。


datas = {
    'api_event': [
        {'id': 1, 'name': '红米Pro发布会'},
        {'id': 2, 'name': '可参加人数为0'},
        {'id': 3, 'name': '当前状态为0关闭'},
        {'id': 4, 'name': '发布会已结束'},
        {'id': 5, 'name': '小米5发布会'},
    ],
    'api_guest': [
        {'id': 1, 'real_name': 'alen'},
        {'id': 2, 'real_name': 'has sign'},
        {'id': 3, 'real_name': 'tom'},
    ]
}

db.init_table(datas)
  • close

关闭数据库连接。

db.close()

最后,基于 seldom 实现接口自动化测试的项目:https://github.com/defnngj/pyrequest2

共收到 54 条回复 时间 点赞

思路不错,学习了

mark 一下。

3楼 已删除

前置和后置有例子吗~~

还有一些问题没有解决掉, 当然比那些 web 版的已经好很多了. 但又因为是 coding 版的 不同公司的定制化又太多. 所以开源出来是否有意义呢? 我自己写的也很早就有发布出来的想法, 但一直都有上述的疑虑.
总之还是给你点个赞!

Jay_ 回复

既然是开源的框架,要解决是一些通用性的问题(断言、报告、参数化), 比如接口的加密,那不同公司的加密规则肯定是不一样的,就需要根据情况自己去封装。

开源首先对自己是有意义的,如何维护到一个开源项目。

如果的开源项目刚好也解决了别人的问题,肯定对别人也是由意义的。

点赞一下

恒温 将本帖设为了精华贴 06月16日 22:55

我感觉接口测试还是测试数据难处理。
比如,我有个订单详情接口需要测试,那传一个订单 id 进去,但是我无法知道我传入的 id 是否存在,那是不是得先生成一个订单,然后拿到这个订单的 id,再去执行订单详情接口的测试用例,如果需要测试不同状态的订单详情,那还得先生成多个订单?
想问下这种是需要怎么处理的?是不是我的思路有问题

buggg 回复

这种算是测试用例里面的前置条件吧,类比测试框架里的 @Before 类方法。这种场景很常见。

至于是每次生成订单,还是写死固定几个不会改状态的订单 id,就要根据实际情况来选择了。

对于电商系统前置依赖还算很小的啦,像我们游戏无论是 UI 还是接口自动化,都是数据构造麻烦,占用的整个用例编写时长一半或往上。比如一个球员升级的功能点,往往需要账号达到等级达到 xx,对应功能建筑等级达到 xx 并解锁了升级玩法,需要有满足要求的球员,升级球员所需要的几个材料数量够足。这些数据准备大部分是通过 sql 来重置少量使用接口处理。编写 UI 或接口 case 的同学都羡慕编写性能 case 的同学,相比之下性能 case 最简单。

虫师 #13 · 2021年06月17日 Author
buggg 回复

正是我文章开头提到的第一点,测试数据问题,这会直接影响接口自动化测试的稳定性。这个问题选择忽略,接口自动化失败是无法断定是 数据导致的,还是真发现了 bug。
一般的做法:

  1. 写个 sql 脚本,在跑自动化之前,初始化数据库。
  2. 调用别的接口还原数据,比如,调用删除接口之前,先调用 添加接口。
  3. 数据银行(我面试时一个应聘者的叫法),其实就是建立一个系统来完成一些测试数据的生成。 ....

seldom 集成了数据操作的目的也会为了解决这个问题,方便的链接数据库去构造一些测试数据。当然,这也仅限于中小规模的系统,以及测试人员对接口涉及到的表接口足够熟悉。

buggg 回复

我们一般的做法是对查询订单的 sql 做一层封装,测试用例可以根据不同的条件来调用,比如订单种类有 pending, payed, itemCount 是多少之类的那我们就可以实行动态查找,getOrder(pending|ItemCount>3) 这种来获取待付款并且商品数量大于 3 个的订单,进行后续的验证,这样可以对用例进行解耦,覆盖率也容易上去。当然对于怎么封装 sql,这个得根据业务来了。

API 测试的话也推荐下用 jsonpath 来处理返回数据,可以减少太多遍历操作了。
https://cloud.tencent.com/developer/article/1511637

alex 回复

想问下你们的接口测试,用例是直接撸代码的,还是用 httprunner 这种用 yaml 文件来驱动的框架呢

buggg 回复

直接撸代码的,用代码感觉更灵活一点吧,熟悉了的话。一些公用的也可以自己根据业务封装起来。

seldom 的 assertPath 断言基于 jmespath 库 ,用法也很强大。https://jmespath.org/specification.html

仅楼主可见
虫师 #20 · 2021年06月19日 Author
reviewtiger 回复

@reviewtiger

1、单接口测试,通过 mock 平台,消除接口之间的依赖,针对每个接口进行测试,当然这需要开发配合对接口的调用做做一些调整。
2、接口场景测试,就如你上面说的 A 接口-->B 接口 --> C 接口 .... 有时候接口的场景构造更复杂,通过 UI 自动化反倒简单一些,可以通过 UI 自动化覆盖这种场景。

请教下大家,在做接口自动化时,比如增删改查的接口除了验证接口返回数据是否正常,还需要同时验证数据库中的数据吗?我们现在接口自动化只验证接口返回数据是否正常,没有对 mysql 或 redis 中的数据进行验证

再请教下大家,对于异步接口大家是怎么验证的,比如接口 A 作用是触发 Kafka 产生一条消息,过了一会后台进程消费该消息并写入数据库,接口 B 可以返回后台进程写到数据库。
1、A 接口和 B 接口用一个自动化用例进行验证?
2、A 接口生产数据后 B 接口需要等待一段时间才能验证数据是否正常,如果在用例中加等待时间会延长整个自动化用例的执行时间(目前 1000 多条用例都是单进程执行的)

调用 diff_json 函数时,类型为 list,list 中元素为字典,sorted(response_data) 会报错 TypeError: '<' not supported between instances of 'dict' and 'dict',这种情况该怎么处理呢

虫师 #24 · 2021年06月26日 Author
文若 回复

@ 文若 感谢,已修复!

Thirty-Thirty 回复

感谢 大佬的解惑

花菜 回复

卧槽,头像都一样,花菜大佬

虫师 回复

写学习一波大佬的分享,多谢分享

比如删除接口,重复执行还能保持结果一致,必定要做数据初始化。

请问数据初始化如何实现?

比如说增加接口,步骤一般是增加 -> 查询并校验增加是否成功,但是如果查询时校验失败,但是第二次执行会出错,因为” 增加了重复的内容 “,那么需要新增前查看是否存在数据,如果存在则将数据删除,那这样步骤变成了:

查看要增加的数据是否存在 -> 如果存在则删除 -> 新增 -> 查询并校验是否新增成功。

但是这种情况下,测试的是新增功能,但是用到了查询、删除接口,是默认这两个接口是正确的吗?可能查询功能可以在最前面校验好,但是删除接口,为了保持重复执行 结果一致,也会用到新增接口,需要默认新增接口正确吗?

小怪兽 回复

可以做上下文策略,在有效等价类阶段。执行某个接口前,判断哪些条件不足返回专门做上文构建的序号,上面满足条件的 case 就是一个序号,然后执行这个序号,另外回包一般可以把二进制转 JSON 也可以用 schema

虫师出品 必属精品

安装 seldom 报错,No such file or directory: 'description.rst',通过 pip 安装和直接下载 tar.gz 包安装都会报这个错误,这就是从入门到放弃吗😂

hancel 回复

自己解决了,把 setup.py 文件里所有 description 删掉就行了

虫师 #34 · 2021年08月19日 Author
hancel 回复

支持标准的 pip 安装:

pip install seldom

setup.py 有引用 description.rst 文件, 项目中两个文件都是存在。

虫师 回复


我也遇到这个问题了!具体怎么解决的?

虫师 #36 · 2021年09月21日 Author
李晔 回复

打包上传 pypi 漏掉了,.whl 文件, 已经在2.2.4 版本修复。

有问题去项目下面提问,更及时得到反馈。
https://github.com/SeldomQA/seldom/issues

中秋节快乐!

虫师 回复

谢谢!已成功安装 2.2.4 版本。对了, 这个库支持 APP 自动化吗?

seldom 中打标机制可以在哪获取到相关资料,seldom 的相关资料很少,有没有资料库。

虫师 #39 · 2021年10月20日 Author
A_延林 回复

项目配的有文档:https://github.com/SeldomQA/seldom/tree/master/docs , 打标机制是什么?可能不支持。

虫师 回复

问题 1、文档我都看过了写的挺好的
问题 2、打标机制 ---就是给 case 做标记,执行时在不同场景下调度不同标记的 case。
感谢虫师的回复

loshu2003 回复

肯定要验证数据库的 这个是必须的 不然有什么意义呢

感谢大佬的文章,学习很多。有个问题请教下,因为公司业务原因,接口测试经验比较少,但是还是想了解下。请问大佬们的公司,接口测试,自己写接口代码比较多,还是靠工具自动生成代码比较多?效率上来说,哪种方式比较推荐?靠工具自动生成代码其实听来很高大上,实施起来有哪些难点阻碍呢?

虫师 #43 · 2022年01月05日 Author

接口测试用例代码 非常模板化,我的意思是只要了解接口(HTTP)的规则,手写 和 生成其实没有区别。1. 手写就是复制、粘贴改。 2、用工具生成,抓包或 浏览器工具 复制 curl 转代码。 难度上:都没有难度,效率上:都差不多。

当然,如果你说的是自动录制回放的,那是另外一条玩法了。

requests.exceptions.MissingSchema: Invalid URL '/api/add_event/': No scheme supplied. Perhaps you meant http:///api/add_event/?

虫师 #45 · 2022年01月12日 Author

@wiggins 去项目下面提问哈! 给出详细的例子和错误描述:https://github.com/SeldomQA/seldom/issues

为什么没有人利用数据库进行加密、生成随机数这些操作?

{
"release":[
{"pageId":18,
"pageVersion":4}
]
}
@file_data('data.json',key='release')
def test_aab():
pass
括号 test_aab 的括号里面应该怎么写能接的住 release 的 json 呢

虫师 #49 · 2022年02月11日 Author

@ 李健彬 首先,确保 seldom 大于 2.4.2 版本,否则,不支持列表嵌套 字典。

  • json
{
"release": [
    {
        "pageId":18,
        "pageVersion":4
    }
 ]
}
  • seldom
@file_data('data.json',key='release')
def test_aab(pageId, pageVersion)
    pass

备注: 参数中的 pageIdpageVersion 并不需要和 json 中的名字对应,但参数顺序有严格要求。

你也可以写成下面这样:

@file_data('data.json',key='release')
def test_aab(pId, pVersion)
    pass
虫师 回复

谢谢。看了版本 低于 2.4.2 😂

兔子🐰 [该话题已被删除] 中提及了此贴 02月18日 16:23

报这个框架的培训班你才能用这个框架。

53楼 已删除
虫师 #54 · 2022年05月13日 Author

@kingTester 先跑到我的 poium 下面评论 这不是 page object ,搞笑(欢迎给我科普一下什么才是 page objects )。 然后,又跑到这里 恶意 评论。 seldomQA 相关项目都是开源的,哪里得出的结论 需要报培训班才能用一说?

这个数据驱动,有点像 testNG

可以请教一下,当初为啥选择了 unittest,而没用 pytest 吗

虫师 #57 · 2023年08月16日 Author
文若 回复

本来打字说原因,后来发现写过一篇文章。直接贴给你。https://mp.weixin.qq.com/s/x9r9ryNu4Ha30lJkWWSavA

seldom 官网也有 seldom vs pytest 对比:https://seldomqa.github.io/introduce.html

虫师 回复

感谢分享

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册