diff --git a/README.md b/README.md index b730e1c5..6fc4eba0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Thank you to all our sponsors! ✨🍰✨ ([become a sponsor](docs/sponsors.md)) ### 金牌赞助商(Gold Sponsor) -[霍格沃兹测试学院](https://testing-studio.com) +[霍格沃兹测试学院](https://testing-studio.com) > [霍格沃兹测试学院](https://testing-studio.com) 是由测吧(北京)科技有限公司与知名软件测试社区 [TesterHome](https://testerhome.com/) 合作的高端教育品牌。由 BAT 一线**测试大咖执教**,提供**实战驱动**的接口自动化测试、移动自动化测试、性能测试、持续集成与 DevOps 等技术培训,以及测试开发优秀人才内推服务。[点击学习!](https://ke.qq.com/course/348893?flowToken=1014523) @@ -59,7 +59,7 @@ Thank you to all our sponsors! ✨🍰✨ ([become a sponsor](docs/sponsors.md)) 关注 HttpRunner 的微信公众号,第一时间获得最新资讯。 -![](docs/qrcode.jpg) +![](docs/assets/qrcode.jpg) [Requests]: http://docs.python-requests.org/en/master/ [unittest]: https://docs.python.org/3/library/unittest.html diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..3c1a8252 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +docs.httprunner.org diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 00000000..b43101d1 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1 @@ +# 常见问题 diff --git a/docs/Installation.md b/docs/Installation.md new file mode 100644 index 00000000..16f1d750 --- /dev/null +++ b/docs/Installation.md @@ -0,0 +1,127 @@ +## 运行环境 + +HttpRunner 是一个基于 Python 开发的测试框架,可以运行在 macOS、Linux、Windows 系统平台上。 + +**Python 版本**:HttpRunner 支持 Python 3.4 及以上的所有版本,并使用 Travis-CI 进行了[持续集成测试][travis-ci],测试覆盖的版本包括 2.7/3.4/3.5/3.6/3.7。虽然 HttpRunner 暂时保留了对 Python 2.7 的兼容支持,但强烈建议使用 Python 3.4 及以上版本。 + +**操作系统**:推荐使用 macOS/Linux。 + +## 安装方式 + +HttpRunner 的稳定版本托管在 PyPI 上,可以使用 `pip` 进行安装。 + +```bash +$ pip install httprunner +``` + +如果你需要使用最新的开发版本,那么可以采用项目的 GitHub 仓库地址进行安装: + +```bash +$ pip install git+https://github.com/HttpRunner/HttpRunner.git@master +``` + +## 版本升级 + +假如你之前已经安装过了 HttpRunner,现在需要升级到最新版本,那么你可以使用`-U`参数。该参数对以上三种安装方式均生效。 + +```bash +$ pip install -U HttpRunner +$ pip install -U git+https://github.com/HttpRunner/HttpRunner.git@master +``` + +## 安装校验 + +在 HttpRunner 安装成功后,系统中会新增如下 5 个命令: + +- `httprunner`: 核心命令 +- `ate`: 曾经用过的命令(当时框架名称为 ApiTestEngine),功能与 httprunner 完全相同 +- `hrun`: httprunner 的缩写,功能与 httprunner 完全相同 +- `locusts`: 基于 [Locust][Locust] 实现[性能测试](run-tests/load-test.md) +- [`har2case`][har2case]: 辅助工具,可将标准通用的 HAR 格式(HTTP Archive)转换为`YAML/JSON`格式的测试用例 + +httprunner、hrun、ate 三个命令完全等价,功能特性完全相同,个人推荐使用`hrun`命令。 + +运行如下命令,若正常显示版本号,则说明 HttpRunner 安装成功。 + +```text +$ hrun -V +2.0.2 + +$ har2case -V +0.2.0 +``` + +## 开发者模式 + +默认情况下,安装 HttpRunner 的时候只会安装运行 HttpRunner 的必要依赖库。 + +如果你不仅仅是使用 HttpRunner,还需要对 HttpRunner 进行开发调试(debug),那么就需要进行如下操作。 + +HttpRunner 使用 [pipenv][pipenv] 对依赖包进行管理,若你还没有安装 pipenv,需要先执行如下命令进行按照: + +```bash +$ pip install pipenv +``` + +获取 HttpRunner 源码: + +```bash +$ git clone https://github.com/HttpRunner/HttpRunner.git +``` + +进入仓库目录,安装所有依赖: + +```bash +$ pipenv install --dev +``` + +运行单元测试,若测试全部通过,则说明环境正常。 + +```bash +$ pipenv run python -m unittest discover +``` + +查看 HttpRunner 的依赖情况: + +```text +$ pipenv graph + +HttpRunner==2.0.0 + - colorama [required: Any, installed: 0.4.0] + - colorlog [required: Any, installed: 3.1.4] + - har2case [required: Any, installed: 0.2.0] + - PyYAML [required: Any, installed: 3.13] + - Jinja2 [required: Any, installed: 2.10] + - MarkupSafe [required: >=0.23, installed: 1.0] + - PyYAML [required: Any, installed: 3.13] + - requests [required: Any, installed: 2.20.0] + - certifi [required: >=2017.4.17, installed: 2018.10.15] + - chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4] + - idna [required: >=2.5,<2.8, installed: 2.7] + - urllib3 [required: >=1.21.1,<1.25, installed: 1.24] + - requests-toolbelt [required: Any, installed: 0.8.0] + - requests [required: >=2.0.1,<3.0.0, installed: 2.20.0] + - certifi [required: >=2017.4.17, installed: 2018.10.15] + - chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4] + - idna [required: >=2.5,<2.8, installed: 2.7] + - urllib3 [required: >=1.21.1,<1.25, installed: 1.24] +``` + +调试运行方式: + +```bash +# 调试运行 hrun +$ pipenv run python main-debug.py hrun -h + +# 调试运行 locusts +$ pipenv run python main-debug.py locusts -h +``` + +## Docker + +TODO + +[travis-ci]: https://travis-ci.org/HttpRunner/HttpRunner +[Locust]: http://locust.io/ +[har2case]: https://github.com/HttpRunner/har2case +[pipenv]: https://docs.pipenv.org/ \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9a4d0708 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,45 @@ +# HttpRunner V2.x 中文使用文档 + +## 在线阅读 + +本文档托管在`GitHub Pages`上,访问地址: + +https://cn.httprunner.org (托管在 GitHub Pages) + +或者 + +https://httprunner.debugtalk.com (托管在七牛云 CDN) + +## 本地预览 + +### 安装依赖 + +本项目文档采用[`mkdocs`][mkdocs]生成,如需在本地预览查看,则需安装该工具。 + +```bash +$ pip install mkdocs +``` + +`mkdocs`支持主题配置,本项目选择了第三方的[`mkdocs-material`][mkdocs-material]。 + +```bash +$ pip install mkdocs-material +``` + +### 启动本地server + +在项目根目录中运行如下命令: + +```bash +$ mkdocs serve +INFO - Building documentation... +INFO - Cleaning site directory +[I 180211 22:48:35 server:283] Serving on http://127.0.0.1:8000 +[I 180211 22:48:35 handlers:60] Start watching changes +[I 180211 22:48:35 handlers:62] Start detecting changes +``` + +然后在浏览器中访问`http://127.0.0.1:8000`即可。 + +[mkdocs]: http://www.mkdocs.org/ +[mkdocs-material]: https://squidfunk.github.io/mkdocs-material/ \ No newline at end of file diff --git a/docs/hogwarts.png b/docs/assets/hogwarts.png similarity index 100% rename from docs/hogwarts.png rename to docs/assets/hogwarts.png diff --git a/docs/qrcode.jpg b/docs/assets/qrcode.jpg similarity index 100% rename from docs/qrcode.jpg rename to docs/assets/qrcode.jpg diff --git a/docs/attachments/DJI-HttpRunner.pdf b/docs/attachments/DJI-HttpRunner.pdf new file mode 100644 index 00000000..d73dcf5c Binary files /dev/null and b/docs/attachments/DJI-HttpRunner.pdf differ diff --git a/docs/attachments/MTSC2019-HttpRunner-2.0.pdf b/docs/attachments/MTSC2019-HttpRunner-2.0.pdf new file mode 100644 index 00000000..a25e8caf Binary files /dev/null and b/docs/attachments/MTSC2019-HttpRunner-2.0.pdf differ diff --git a/docs/attachments/PyCon-HttpRunner.pdf b/docs/attachments/PyCon-HttpRunner.pdf new file mode 100644 index 00000000..a5073f79 Binary files /dev/null and b/docs/attachments/PyCon-HttpRunner.pdf differ diff --git a/docs/concept/nominal.md b/docs/concept/nominal.md new file mode 100644 index 00000000..3d6af631 --- /dev/null +++ b/docs/concept/nominal.md @@ -0,0 +1,67 @@ +## 测试用例(testcase) + +从 2.0 版本开始,HttpRunner 开始对测试用例的定义进行进一步的明确,参考 [wiki][wiki_testcase] 上的描述。 + +> A test case is a specification of the inputs, execution conditions, testing procedure, and expected results that define a single test to be executed to achieve a particular software testing objective, such as to exercise a particular program path or to verify compliance with a specific requirement. + +概括下来,一条测试用例(testcase)应该是为了测试某个特定的功能逻辑而精心设计的,并且至少包含如下几点: + +- 明确的测试目的(achieve a particular software testing objective) +- 明确的输入(inputs) +- 明确的运行环境(execution conditions) +- 明确的测试步骤描述(testing procedure) +- 明确的预期结果(expected results) + +对应地,HttpRunner 的测试用例描述方式进行如下设计: + +- 测试用例应该是完整且独立的,每条测试用例应该是都可以独立运行的;在 HttpRunner 中,每个 `YAML/JSON` 文件对应一条测试用例。 +- 测试用例包含 `测试脚本` 和 `测试数据` 两部分: + - `测试用例 = 测试脚本 + 测试数据` + - `测试脚本` 重点是描述测试的 `业务功能逻辑`,包括预置条件、测试步骤、预期结果等,并且可以结合辅助函数(debugtalk.py)实现复杂的运算逻辑;可以将 `测试脚本` 理解为编程语言中的 `类(class)`; + - `测试数据` 重点是对应测试的 `业务数据逻辑`,可以理解为类的实例化数据; + - `测试数据` 和 `测试脚本` 分离后,就可以比较方便地实现数据驱动测试,通过对测试脚本传入一组数据,实现同一业务功能在不同数据逻辑下的测试验证。 + + +## 测试步骤(teststep) + +测试用例是测试步骤的 `有序` 集合,而对于接口测试来说,每一个测试步骤应该就对应一个 API 的请求描述。 + +## 测试用例集(testsuite) + +`测试用例集` 是 `测试用例` 的 `无序` 集合,集合中的测试用例应该都是相互独立,不存在先后依赖关系的。 + +如果确实存在先后依赖关系怎么办,例如登录功能和下单功能。正确的做法应该是,在下单测试用例的前置步骤中执行登录操作。 + +```yaml +- config: + name: order product + +- test: + name: login + testcase: testcases/login.yml + +- test: + name: add to cart + api: api/add_cart.yml + +- test: + name: make order + api: api/make_order.yml +``` + +## 测试场景 + +`测试场景` 和 `测试用例集` 是同一概念,都是 `测试用例` 的 `无序` 集合。 + + +- 接口 +- 测试用例集 +- 参数 +- 变量 +- 测试脚本(YAML/JSON) +- debugtalk.py +- 环境变量 + +## 项目根目录 + +[wiki_testcase]: https://en.wikipedia.org/wiki/Test_case \ No newline at end of file diff --git a/docs/data/account.csv b/docs/data/account.csv new file mode 100644 index 00000000..2502b5b8 --- /dev/null +++ b/docs/data/account.csv @@ -0,0 +1,4 @@ +username,password,phone +test1,111111,18600000001 +test2,222222,18600000002 +test3,333333,18600000003 \ No newline at end of file diff --git a/docs/data/api_server.py b/docs/data/api_server.py new file mode 100644 index 00000000..bff04ddf --- /dev/null +++ b/docs/data/api_server.py @@ -0,0 +1,223 @@ +import hashlib +import hmac +import json +import random +import string +from functools import wraps + +from flask import Flask, make_response, request + +SECRET_KEY = "DebugTalk" + +app = Flask(__name__) + +""" storage all users' data +data structure: + users_dict = { + 'uid1': { + 'name': 'name1', + 'password': 'pwd1' + }, + 'uid2': { + 'name': 'name2', + 'password': 'pwd2' + } + } +""" +users_dict = {} + +""" storage all token data +data structure: + token_dict = { + 'device_sn1': 'token1', + 'device_sn2': 'token1' + } +""" +token_dict = {} + + +def gen_random_string(str_len): + """ generate random string with specified length + """ + return ''.join( + random.choice(string.ascii_letters + string.digits) for _ in range(str_len)) + +def get_sign(*args): + content = ''.join(args).encode('ascii') + sign_key = SECRET_KEY.encode('ascii') + sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest() + return sign + +def gen_md5(*args): + return hashlib.md5("".join(args).encode('utf-8')).hexdigest() + + +def validate_request(func): + + @wraps(func) + def wrapper(*args, **kwargs): + device_sn = request.headers.get('device_sn', "") + token = request.headers.get('token', "") + + if not device_sn or not token: + result = { + 'success': False, + 'msg': "device_sn or token is null." + } + response = make_response(json.dumps(result), 401) + response.headers["Content-Type"] = "application/json" + return response + + if token_dict.get(device_sn) != token: + result = { + 'success': False, + 'msg': "Authorization failed!" + } + response = make_response(json.dumps(result), 403) + response.headers["Content-Type"] = "application/json" + return response + + return func(*args, **kwargs) + + return wrapper + + +@app.route('/') +def index(): + return "Hello World!" + +@app.route('/api/get-token', methods=['POST']) +def get_token(): + device_sn = request.headers.get('device_sn', "") + os_platform = request.headers.get('os_platform', "") + app_version = request.headers.get('app_version', "") + data = request.get_json() + sign = data.get('sign', "") + + expected_sign = get_sign(device_sn, os_platform, app_version) + + if expected_sign != sign: + result = { + 'success': False, + 'msg': "Authorization failed!" + } + response = make_response(json.dumps(result), 403) + else: + token = gen_random_string(16) + token_dict[device_sn] = token + + result = { + 'success': True, + 'token': token + } + response = make_response(json.dumps(result)) + + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/users') +@validate_request +def get_users(): + users_list = [user for uid, user in users_dict.items()] + users = { + 'success': True, + 'count': len(users_list), + 'items': users_list + } + response = make_response(json.dumps(users)) + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/reset-all') +@validate_request +def clear_users(): + users_dict.clear() + result = { + 'success': True + } + response = make_response(json.dumps(result)) + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/users/', methods=['POST']) +@validate_request +def create_user(uid): + user = request.get_json() + if uid not in users_dict: + result = { + 'success': True, + 'msg': "user created successfully." + } + status_code = 201 + users_dict[uid] = user + else: + result = { + 'success': False, + 'msg': "user already existed." + } + status_code = 500 + + response = make_response(json.dumps(result), status_code) + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/users/') +@validate_request +def get_user(uid): + user = users_dict.get(uid, {}) + if user: + result = { + 'success': True, + 'data': user + } + status_code = 200 + else: + result = { + 'success': False, + 'data': user + } + status_code = 404 + + response = make_response(json.dumps(result), status_code) + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/users/', methods=['PUT']) +@validate_request +def update_user(uid): + user = users_dict.get(uid, {}) + if user: + user = request.get_json() + success = True + status_code = 200 + users_dict[uid] = user + else: + success = False + status_code = 404 + + result = { + 'success': success, + 'data': user + } + response = make_response(json.dumps(result), status_code) + response.headers["Content-Type"] = "application/json" + return response + +@app.route('/api/users/', methods=['DELETE']) +@validate_request +def delete_user(uid): + user = users_dict.pop(uid, {}) + if user: + success = True + status_code = 200 + else: + success = False + status_code = 404 + + result = { + 'success': success, + 'data': user + } + response = make_response(json.dumps(result), status_code) + response.headers["Content-Type"] = "application/json" + return response diff --git a/docs/data/app_version.csv b/docs/data/app_version.csv new file mode 100644 index 00000000..a0c236a4 --- /dev/null +++ b/docs/data/app_version.csv @@ -0,0 +1,3 @@ +app_version +2.8.5 +2.8.6 diff --git a/docs/data/debugtalk.py b/docs/data/debugtalk.py new file mode 100644 index 00000000..53b56d02 --- /dev/null +++ b/docs/data/debugtalk.py @@ -0,0 +1,48 @@ +import hashlib +import hmac +import random +import string +import time + +SECRET_KEY = "DebugTalk" + +def gen_random_string(str_len): + random_char_list = [] + for _ in range(str_len): + random_char = random.choice(string.ascii_letters + string.digits) + random_char_list.append(random_char) + + random_string = ''.join(random_char_list) + return random_string + +def get_sign(*args): + content = ''.join(args).encode('ascii') + sign_key = SECRET_KEY.encode('ascii') + sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest() + return sign + +def gen_user_id(): + return int(time.time() * 1000) + +def get_user_id(): + return [ + {"user_id": 1001}, + {"user_id": 1002}, + {"user_id": 1003}, + {"user_id": 1004} + ] + +def get_account(num): + accounts = [] + for index in range(1, num+1): + accounts.append( + {"username": "user%s" % index, "password": str(index) * 6}, + ) + + return accounts + +def get_os_platform(): + return [ + {"os_platform": "ios"}, + {"os_platform": "android"} + ] diff --git a/docs/data/demo-parameters-get-token.yml b/docs/data/demo-parameters-get-token.yml new file mode 100644 index 00000000..d1bfb633 --- /dev/null +++ b/docs/data/demo-parameters-get-token.yml @@ -0,0 +1,10 @@ +config: + name: get token with parameters + +testcases: + get token with $user_agent, $app_version, $os_platform: + testcase: demo-testcase-get-token.yml + parameters: + user_agent: ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + app_version: ${P(app_version.csv)} + os_platform: ${get_os_platform()} diff --git a/docs/data/demo-quickstart-0.json b/docs/data/demo-quickstart-0.json new file mode 100644 index 00000000..57dd4ce9 --- /dev/null +++ b/docs/data/demo-quickstart-0.json @@ -0,0 +1,58 @@ +[ + { + "config": { + "name": "testcase description", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.token", "baNLX1zhFYP11Seb"]} + ] + } + }, + { + "test": { + "name": "/api/users/1000", + "request": { + "url": "http://127.0.0.1:5000/api/users/1000", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "token": "baNLX1zhFYP11Seb", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart-0.yml b/docs/data/demo-quickstart-0.yml new file mode 100644 index 00000000..9e50c8b6 --- /dev/null +++ b/docs/data/demo-quickstart-0.yml @@ -0,0 +1,41 @@ +- config: + name: testcase description + variables: {} + +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: http://127.0.0.1:5000/api/get-token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.token, baNLX1zhFYP11Seb] + +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: baNLX1zhFYP11Seb + json: + name: user1 + password: '123456' + method: POST + url: http://127.0.0.1:5000/api/users/1000 + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] \ No newline at end of file diff --git a/docs/data/demo-quickstart-1.json b/docs/data/demo-quickstart-1.json new file mode 100644 index 00000000..433666c8 --- /dev/null +++ b/docs/data/demo-quickstart-1.json @@ -0,0 +1,57 @@ +[ + { + "config": { + "name": "testcase description", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/1000", + "request": { + "url": "http://127.0.0.1:5000/api/users/1000", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "token": "baNLX1zhFYP11Seb", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart-1.yml b/docs/data/demo-quickstart-1.yml new file mode 100644 index 00000000..1874c75d --- /dev/null +++ b/docs/data/demo-quickstart-1.yml @@ -0,0 +1,40 @@ +- config: + name: testcase description + variables: {} + +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: http://127.0.0.1:5000/api/get-token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: baNLX1zhFYP11Seb + json: + name: user1 + password: '123456' + method: POST + url: http://127.0.0.1:5000/api/users/1000 + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] \ No newline at end of file diff --git a/docs/data/demo-quickstart-2.json b/docs/data/demo-quickstart-2.json new file mode 100644 index 00000000..6c24da28 --- /dev/null +++ b/docs/data/demo-quickstart-2.json @@ -0,0 +1,60 @@ +[ + { + "config": { + "name": "testcase description", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/1000", + "request": { + "url": "http://127.0.0.1:5000/api/users/1000", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "token": "$token", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] diff --git a/docs/data/demo-quickstart-2.yml b/docs/data/demo-quickstart-2.yml new file mode 100644 index 00000000..17924f0d --- /dev/null +++ b/docs/data/demo-quickstart-2.yml @@ -0,0 +1,42 @@ +- config: + name: testcase description + variables: {} + +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: http://127.0.0.1:5000/api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: $token + json: + name: user1 + password: '123456' + method: POST + url: http://127.0.0.1:5000/api/users/1000 + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] diff --git a/docs/data/demo-quickstart-3.json b/docs/data/demo-quickstart-3.json new file mode 100644 index 00000000..fd276ca7 --- /dev/null +++ b/docs/data/demo-quickstart-3.json @@ -0,0 +1,61 @@ +[ + { + "config": { + "name": "testcase description", + "base_url": "http://127.0.0.1:5000", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/1000", + "request": { + "url": "/api/users/1000", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "token": "$token", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] diff --git a/docs/data/demo-quickstart-3.yml b/docs/data/demo-quickstart-3.yml new file mode 100644 index 00000000..654a383b --- /dev/null +++ b/docs/data/demo-quickstart-3.yml @@ -0,0 +1,43 @@ +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + variables: {} + +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: $token + json: + name: user1 + password: '123456' + method: POST + url: /api/users/1000 + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] diff --git a/docs/data/demo-quickstart-4.json b/docs/data/demo-quickstart-4.json new file mode 100644 index 00000000..ef03348e --- /dev/null +++ b/docs/data/demo-quickstart-4.json @@ -0,0 +1,70 @@ +[ + { + "config": { + "name": "testcase description", + "base_url": "http://127.0.0.1:5000", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "variables": { + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6" + }, + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "os_platform": "$os_platform", + "app_version": "$app_version", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/$user_id", + "variables": { + "device_sn": "FwgRiO7CNA50DSU", + "user_id": "1000" + }, + "request": { + "url": "/api/users/$user_id", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "token": "$token", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart-4.yml b/docs/data/demo-quickstart-4.yml new file mode 100644 index 00000000..514b25f6 --- /dev/null +++ b/docs/data/demo-quickstart-4.yml @@ -0,0 +1,50 @@ +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + variables: {} + +- test: + name: /api/get-token + variables: + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: $app_version + device_sn: $device_sn + os_platform: $os_platform + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/$user_id + variables: + device_sn: FwgRiO7CNA50DSU + user_id: 1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: $device_sn + token: $token + json: + name: user1 + password: '123456' + method: POST + url: /api/users/$user_id + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] diff --git a/docs/data/demo-quickstart-5.json b/docs/data/demo-quickstart-5.json new file mode 100644 index 00000000..4ede41f7 --- /dev/null +++ b/docs/data/demo-quickstart-5.json @@ -0,0 +1,70 @@ +[ + { + "config": { + "name": "testcase description", + "base_url": "http://127.0.0.1:5000", + "variables": { + "device_sn": "FwgRiO7CNA50DSU" + } + } + }, + { + "test": { + "name": "/api/get-token", + "variables": { + "os_platform": "ios", + "app_version": "2.8.6" + }, + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "os_platform": "$os_platform", + "app_version": "$app_version", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/$user_id", + "variables": { + "user_id": "1000" + }, + "request": { + "url": "/api/users/$user_id", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "token": "$token", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart-5.yml b/docs/data/demo-quickstart-5.yml new file mode 100644 index 00000000..98445b7f --- /dev/null +++ b/docs/data/demo-quickstart-5.yml @@ -0,0 +1,49 @@ +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + variables: + device_sn: FwgRiO7CNA50DSU + +- test: + name: /api/get-token + variables: + app_version: 2.8.6 + os_platform: ios + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: $app_version + device_sn: $device_sn + os_platform: $os_platform + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/$user_id + variables: + user_id: 1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: $device_sn + token: $token + json: + name: user1 + password: '123456' + method: POST + url: /api/users/$user_id + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] diff --git a/docs/data/demo-quickstart-6.json b/docs/data/demo-quickstart-6.json new file mode 100644 index 00000000..da126fca --- /dev/null +++ b/docs/data/demo-quickstart-6.json @@ -0,0 +1,70 @@ +[ + { + "config": { + "name": "testcase description", + "base_url": "http://127.0.0.1:5000", + "variables": { + "device_sn": "${gen_random_string(15)}" + } + } + }, + { + "test": { + "name": "/api/get-token", + "variables": { + "os_platform": "ios", + "app_version": "2.8.6" + }, + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "os_platform": "$os_platform", + "app_version": "$app_version", + "Content-Type": "application/json" + }, + "json": { + "sign": "${get_sign($device_sn, $os_platform, $app_version)}" + } + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + } + }, + { + "test": { + "name": "/api/users/$user_id", + "variables": { + "user_id": "${gen_user_id()}" + }, + "request": { + "url": "/api/users/$user_id", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "$device_sn", + "token": "$token", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart-6.yml b/docs/data/demo-quickstart-6.yml new file mode 100644 index 00000000..b5acf937 --- /dev/null +++ b/docs/data/demo-quickstart-6.yml @@ -0,0 +1,49 @@ +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + variables: + device_sn: ${gen_random_string(15)} + +- test: + name: /api/get-token + variables: + app_version: 2.8.6 + os_platform: ios + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: $app_version + device_sn: $device_sn + os_platform: $os_platform + json: + sign: ${get_sign($device_sn, $os_platform, $app_version)} + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + +- test: + name: /api/users/$user_id + variables: + user_id: ${gen_user_id()} + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: $device_sn + token: $token + json: + name: user1 + password: '123456' + method: POST + url: /api/users/$user_id + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] diff --git a/docs/data/demo-quickstart-7.json b/docs/data/demo-quickstart-7.json new file mode 100644 index 00000000..89d57f8d --- /dev/null +++ b/docs/data/demo-quickstart-7.json @@ -0,0 +1,13 @@ +{ + "config": { + "name": "create users with parameters" + }, + "testcases": { + "create user $user_id": { + "testcase": "demo-quickstart-6.yml", + "parameters": { + "user_id": [1001, 1002, 1003, 1004] + } + } + } +} \ No newline at end of file diff --git a/docs/data/demo-quickstart-7.yml b/docs/data/demo-quickstart-7.yml new file mode 100644 index 00000000..770dc45f --- /dev/null +++ b/docs/data/demo-quickstart-7.yml @@ -0,0 +1,8 @@ +config: + name: testcase description + +testcases: + create user $user_id: + testcase: demo-quickstart-6.yml + parameters: + user_id: [1001, 1002, 1003, 1004] diff --git a/docs/data/demo-quickstart.har b/docs/data/demo-quickstart.har new file mode 100644 index 00000000..d4924ba4 --- /dev/null +++ b/docs/data/demo-quickstart.har @@ -0,0 +1,221 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Charles Proxy", + "version": "4.2.1" + }, + "entries": [ + { + "startedDateTime": "2018-02-19T17:30:00.904+08:00", + "time": 3, + "request": { + "method": "POST", + "url": "http://127.0.0.1:5000/api/get-token", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:5000" + }, + { + "name": "User-Agent", + "value": "python-requests/2.18.4" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "device_sn", + "value": "FwgRiO7CNA50DSU" + }, + { + "name": "os_platform", + "value": "ios" + }, + { + "name": "app_version", + "value": "2.8.6" + }, + { + "name": "Content-Length", + "value": "52" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ], + "queryString": [], + "postData": { + "mimeType": "application/json", + "text": "{\"sign\": \"9c0c7e51c91ae963c833a4ccbab8d683c4a90c98\"}" + }, + "headersSize": 299, + "bodySize": 52 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.0", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Content-Length", + "value": "46" + }, + { + "name": "Server", + "value": "Werkzeug/0.14.1 Python/3.6.4" + }, + { + "name": "Date", + "value": "Mon, 19 Feb 2018 09:30:00 GMT" + }, + { + "name": "Proxy-Connection", + "value": "Close" + } + ], + "content": { + "size": 46, + "mimeType": "application/json", + "text": "eyJzdWNjZXNzIjogdHJ1ZSwgInRva2VuIjogImJhTkxYMXpoRllQMTFTZWIifQ==", + "encoding": "base64" + }, + "redirectURL": null, + "headersSize": 175, + "bodySize": 46 + }, + "serverIPAddress": "127.0.0.1", + "cache": {}, + "timings": { + "dns": 1, + "connect": 0, + "ssl": -1, + "send": 0, + "wait": 1, + "receive": 1 + } + }, + { + "startedDateTime": "2018-02-19T17:30:00.911+08:00", + "time": 3, + "request": { + "method": "POST", + "url": "http://127.0.0.1:5000/api/users/1000", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:5000" + }, + { + "name": "User-Agent", + "value": "python-requests/2.18.4" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "device_sn", + "value": "FwgRiO7CNA50DSU" + }, + { + "name": "token", + "value": "baNLX1zhFYP11Seb" + }, + { + "name": "Content-Length", + "value": "39" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ], + "queryString": [], + "postData": { + "mimeType": "application/json", + "text": "{\"name\": \"user1\", \"password\": \"123456\"}" + }, + "headersSize": 265, + "bodySize": 39 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 201, + "statusText": "CREATED", + "httpVersion": "HTTP/1.0", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Content-Length", + "value": "54" + }, + { + "name": "Server", + "value": "Werkzeug/0.14.1 Python/3.6.4" + }, + { + "name": "Date", + "value": "Mon, 19 Feb 2018 09:30:00 GMT" + }, + { + "name": "Proxy-Connection", + "value": "Close" + } + ], + "content": { + "size": 54, + "mimeType": "application/json", + "text": "eyJzdWNjZXNzIjogdHJ1ZSwgIm1zZyI6ICJ1c2VyIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5LiJ9", + "encoding": "base64" + }, + "redirectURL": null, + "headersSize": 77, + "bodySize": 54 + }, + "serverIPAddress": "127.0.0.1", + "cache": {}, + "timings": { + "dns": 0, + "connect": 0, + "ssl": -1, + "send": 0, + "wait": 3, + "receive": 0 + } + } + ] + } +} \ No newline at end of file diff --git a/docs/data/demo-quickstart.json b/docs/data/demo-quickstart.json new file mode 100644 index 00000000..88933729 --- /dev/null +++ b/docs/data/demo-quickstart.json @@ -0,0 +1,98 @@ +[ + { + "config": { + "name": "testcase description", + "variables": {} + } + }, + { + "test": { + "name": "/api/get-token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "os_platform": "ios", + "app_version": "2.8.6", + "Content-Type": "application/json" + }, + "json": { + "sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98" + } + }, + "validate": [ + { + "eq": [ + "status_code", + 200 + ] + }, + { + "eq": [ + "headers.Content-Type", + "application/json" + ] + }, + { + "eq": [ + "content.success", + true + ] + }, + { + "eq": [ + "content.token", + "baNLX1zhFYP11Seb" + ] + } + ] + } + }, + { + "test": { + "name": "/api/users/1000", + "request": { + "url": "http://127.0.0.1:5000/api/users/1000", + "method": "POST", + "headers": { + "User-Agent": "python-requests/2.18.4", + "device_sn": "FwgRiO7CNA50DSU", + "token": "baNLX1zhFYP11Seb", + "Content-Type": "application/json" + }, + "json": { + "name": "user1", + "password": "123456" + } + }, + "validate": [ + { + "eq": [ + "status_code", + 201 + ] + }, + { + "eq": [ + "headers.Content-Type", + "application/json" + ] + }, + { + "eq": [ + "content.success", + true + ] + }, + { + "eq": [ + "content.msg", + "user created successfully." + ] + } + ] + } + } +] \ No newline at end of file diff --git a/docs/data/demo-quickstart.yml b/docs/data/demo-quickstart.yml new file mode 100644 index 00000000..6eb2f64c --- /dev/null +++ b/docs/data/demo-quickstart.yml @@ -0,0 +1,55 @@ +- config: + name: testcase description + variables: {} +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: http://127.0.0.1:5000/api/get-token + validate: + - eq: + - status_code + - 200 + - eq: + - headers.Content-Type + - application/json + - eq: + - content.success + - true + - eq: + - content.token + - baNLX1zhFYP11Seb +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: baNLX1zhFYP11Seb + json: + name: user1 + password: '123456' + method: POST + url: http://127.0.0.1:5000/api/users/1000 + validate: + - eq: + - status_code + - 201 + - eq: + - headers.Content-Type + - application/json + - eq: + - content.success + - true + - eq: + - content.msg + - user created successfully. diff --git a/docs/data/demo-testcase-get-token.yml b/docs/data/demo-testcase-get-token.yml new file mode 100644 index 00000000..32db8d1e --- /dev/null +++ b/docs/data/demo-testcase-get-token.yml @@ -0,0 +1,27 @@ +- config: + name: get token + base_url: http://127.0.0.1:5000 + variables: + device_sn: ${gen_random_string(15)} + os_platform: 'ios' + app_version: '2.8.6' + +- test: + name: get token with $device_sn, $os_platform, $app_version + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: $app_version + device_sn: $device_sn + os_platform: $os_platform + json: + sign: ${get_sign($device_sn, $os_platform, $app_version)} + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] diff --git a/docs/data/demo_parameters.yml b/docs/data/demo_parameters.yml new file mode 100644 index 00000000..76a8a4b9 --- /dev/null +++ b/docs/data/demo_parameters.yml @@ -0,0 +1,34 @@ +- config: + name: "user management testcase." + parameters: + - user_agent: ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + - app_version: ${P(app_version.csv)} + - os_platform: ${get_os_platform()} + variables: + - user_agent: 'iOS/10.3' + - device_sn: ${gen_random_string(15)} + - os_platform: 'ios' + - app_version: '2.8.6' + request: + base_url: http://127.0.0.1:5000 + headers: + Content-Type: application/json + device_sn: $device_sn + +- test: + name: get token with $user_agent, $os_platform, $app_version + request: + url: /api/get-token + method: POST + headers: + app_version: $app_version + os_platform: $os_platform + user_agent: $user_agent + json: + sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)} + extract: + - token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] \ No newline at end of file diff --git a/docs/data/testerhome-login.har b/docs/data/testerhome-login.har new file mode 100644 index 00000000..f54e6707 --- /dev/null +++ b/docs/data/testerhome-login.har @@ -0,0 +1 @@ +{"log":{"version":"1.2","creator":{"name":"Charles Proxy","version":"4.2.6"},"entries":[{"startedDateTime":"2019-04-19T14:14:14.014+08:00","time":227,"request":{"method":"GET","url":"https://testerhome.com/account/sign_in","httpVersion":"HTTP/1.1","cookies":[{"name":"_ga","value":"GA1.2.162109905.1516957848"},{"name":"gsScrollPos-3842","value":"0"},{"name":"gsScrollPos-3871","value":""},{"name":"gsScrollPos-1094","value":"0"},{"name":"gsScrollPos-1837","value":"0"},{"name":"gsScrollPos-1993","value":""},{"name":"gsScrollPos-324","value":"0"},{"name":"gsScrollPos-861","value":"0"},{"name":"gsScrollPos-1568","value":"0"},{"name":"gsScrollPos-2909","value":"0"},{"name":"gsScrollPos-2429","value":""},{"name":"gsScrollPos-5380","value":"0"},{"name":"gsScrollPos-5417","value":"0"},{"name":"gsScrollPos-1937750581","value":""},{"name":"gsScrollPos-1937751827","value":"0"},{"name":"gsScrollPos-1937751846","value":"0"},{"name":"gsScrollPos-1937756244","value":"0"},{"name":"gsScrollPos-1937759407","value":"0"},{"name":"gsScrollPos-968","value":"0"},{"name":"gsScrollPos-3085","value":""},{"name":"hasSkipWelcomePage","value":"1"},{"name":"gsScrollPos-1874","value":"0"},{"name":"gsScrollPos-2049","value":"0"},{"name":"gsScrollPos-3406","value":""},{"name":"user_id","value":"NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f"},{"name":"gsScrollPos-891","value":"0"},{"name":"gsScrollPos-931","value":""},{"name":"gsScrollPos-2436","value":"0"},{"name":"_gid","value":"GA1.2.113994526.1555584877"},{"name":"_homeland_session","value":"pMGgyE29RXzrANx5RkqLjCMIE%2FHB%2FxbtHPs1pETwS54JTS%2BaV7QSR10JxAXa6wXxVIKDfMclOhVyQF0ztWr51Z3pEDEZ8P5HRgW7UnnUXHU9LqyAA%2FLF8V3LptG6jYLODkS43KqwfgHQfq1oF9X%2FLDyuYhyfJH%2FQFJLanFfH7lq%2Bg6wJXSVBTvDDZh8m5wITIVhYd63fo8B5Eu3XkSbPpY%2B3PivBAyRiC5AjXEWnhDTDIGA%2BYXZ5hjGIOJRdhjhD1nF5OPq%2FNfG96u6yp5EBWbDknUicZ2YHGQ%3D%3D--G4cowQKL8hiscX9U--48zlnNPSvWTk2tgnQlIhzg%3D%3D"},{"name":"_gat","value":"1"}],"headers":[{"name":"Host","value":"testerhome.com"},{"name":"Connection","value":"keep-alive"},{"name":"Cache-Control","value":"max-age=0"},{"name":"Upgrade-Insecure-Requests","value":"1"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"},{"name":"Accept","value":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"},{"name":"Referer","value":"https://testerhome.com/notifications/personal"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Accept-Language","value":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"},{"name":"Cookie","value":"_ga=GA1.2.162109905.1516957848; gsScrollPos-3842=0; gsScrollPos-3871=; gsScrollPos-1094=0; gsScrollPos-1837=0; gsScrollPos-1993=; gsScrollPos-324=0; gsScrollPos-861=0; gsScrollPos-1568=0; gsScrollPos-2909=0; gsScrollPos-2429=; gsScrollPos-5380=0; gsScrollPos-5417=0; gsScrollPos-1937750581=; gsScrollPos-1937751827=0; gsScrollPos-1937751846=0; gsScrollPos-1937756244=0; gsScrollPos-1937759407=0; gsScrollPos-968=0; gsScrollPos-3085=; hasSkipWelcomePage=1; gsScrollPos-1874=0; gsScrollPos-2049=0; gsScrollPos-3406=; user_id=NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f; gsScrollPos-891=0; gsScrollPos-931=; gsScrollPos-2436=0; _gid=GA1.2.113994526.1555584877; _homeland_session=pMGgyE29RXzrANx5RkqLjCMIE%2FHB%2FxbtHPs1pETwS54JTS%2BaV7QSR10JxAXa6wXxVIKDfMclOhVyQF0ztWr51Z3pEDEZ8P5HRgW7UnnUXHU9LqyAA%2FLF8V3LptG6jYLODkS43KqwfgHQfq1oF9X%2FLDyuYhyfJH%2FQFJLanFfH7lq%2Bg6wJXSVBTvDDZh8m5wITIVhYd63fo8B5Eu3XkSbPpY%2B3PivBAyRiC5AjXEWnhDTDIGA%2BYXZ5hjGIOJRdhjhD1nF5OPq%2FNfG96u6yp5EBWbDknUicZ2YHGQ%3D%3D--G4cowQKL8hiscX9U--48zlnNPSvWTk2tgnQlIhzg%3D%3D; _gat=1"},{"name":"If-None-Match","value":"W/\"bc9ae267fdcbd89bf1dfaea10dea2b0e\""}],"queryString":[],"headersSize":1666,"bodySize":0},"response":{"_charlesStatus":"COMPLETE","status":200,"statusText":"OK","httpVersion":"HTTP/1.1","cookies":[{"name":"_homeland_session","value":"%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D","path":"/","domain":null,"expires":"Thu, 18 Jul 2019 06:14:16 -0000","httpOnly":true,"secure":true,"comment":null,"_maxAge":null}],"headers":[{"name":"Server","value":"nginx/1.10.2"},{"name":"Date","value":"Fri, 19 Apr 2019 06:14:16 GMT"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Accept-Encoding"},{"name":"X-Frame-Options","value":"SAMEORIGIN"},{"name":"X-XSS-Protection","value":"1; mode=block"},{"name":"X-Content-Type-Options","value":"nosniff"},{"name":"X-Download-Options","value":"noopen"},{"name":"X-Permitted-Cross-Domain-Policies","value":"none"},{"name":"Referrer-Policy","value":"strict-origin-when-cross-origin"},{"name":"ETag","value":"W/\"587c0b9f9cf24943552b421690023012\""},{"name":"Cache-Control","value":"max-age=0, private, must-revalidate"},{"name":"Content-Security-Policy","value":";"},{"name":"Set-Cookie","value":"_homeland_session=%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D; path=/; expires=Thu, 18 Jul 2019 06:14:16 -0000; secure; HttpOnly"},{"name":"X-Request-Id","value":"d12cd1e4-2289-428f-8755-cabf6ad974ab"},{"name":"X-Runtime","value":"0.041729"},{"name":"Strict-Transport-Security","value":"max-age=15552000; includeSubDomains"},{"name":"Content-Encoding","value":"gzip"},{"name":"Connection","value":"keep-alive"}],"content":{"size":12610,"compression":8072,"mimeType":"text/html; charset=utf-8","text":"\n\n\n\n \n \n \n \n Sign In · TesterHome<\/title>\n <link rel=\"icon\" href=\"/assets/favicon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"apple-touch-icon-precomposed\" href=\"/assets/ios-icon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"shortcut icon\" href=\"/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"apple-touch-icon\" href=\"/assets/favicon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\">\n\n <!-- http://www.favicon-generator.org/ -->\n <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"/apple-icon-57x57.png\">\n <link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"/apple-icon-60x60.png\">\n <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"/apple-icon-72x72.png\">\n <link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"/apple-icon-76x76.png\">\n <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"/apple-icon-114x114.png\">\n <link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"/apple-icon-120x120.png\">\n <link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"/apple-icon-144x144.png\">\n <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"/apple-icon-152x152.png\">\n <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-icon-180x180.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/android-icon-192x192.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"/favicon-96x96.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n <link rel=\"manifest\" href=\"/manifest.json\">\n <meta name=\"msapplication-TileColor\" content=\"#ffffff\">\n <meta name=\"msapplication-TileImage\" content=\"/ms-icon-144x144.png\">\n <meta name=\"theme-color\" content=\"#ffffff\">\n\n <meta name=\"apple-mobile-web-app-capable\" content=\"no\">\n <meta content='True' name='HandheldFriendly'/>\n <link rel=\"alternate\" type=\"application/rss+xml\" title=\"订阅最新帖\" href=\"https://testerhome.com/topics/feed\"/>\n <link rel=\"stylesheet\" media=\"screen\" href=\"/assets/front-15185e3983cd68677b04329670c6a6b7fedecb51f1b39ff7fac48180c1850eaa.css\" data-turbolinks-track=\"reload\" />\n \n \n <meta name=\"action-cable-url\" content=\"/cable\" />\n <meta name=\"csrf-param\" content=\"authenticity_token\" />\n<meta name=\"csrf-token\" content=\"0zAKFDDPnNI2+Vwq/iwDPR9vo7KWobfNLAye4EaGBTlsSxMzTNf39lLF9z35f5mcROM7JgOP+azBCuDe84G+XA==\" />\n <meta itemprop=\"name\" content=\"TesterHome\"/>\r\n <meta itemprop=\"image\" content=\"//10.url.cn/qqcourse_logo_ng/ajNVdqHZLLAH8YXbXMDFib2SnIqhac60vw3BspyLd0TO82PiaJ2xfbvXynrocic7ajbCGribAic88wgc/\"/>\r\n\r\n<style type=\"text/css\">\r\n\r\n.blog-description{\r\n text-align: left;\r\n padding: 18px;\r\n}\r\n.blog-description:first-letter {\r\nfont-size : 200%;\r\nfont-weight : bold;\r\nfloat : left;\r\nmargin-right: 3px;\r\n}\r\n\r\n/*\r\n.new-topic:before {\r\n content:url(\"https://testerhome.com/uploads/photo/2019/d80c7d7c-7727-45b2-a98d-2c87a68c65e8.png\");\r\n position: absolute;\r\n left:60%;\r\n margin-top:-35px;\r\n}\r\n*/\r\n<\/style>\n\n <script src=\"/assets/app-04f83676fc1f6160a4410556b3298892a4536d9e8ee6c9b54df9191dd8dd9060.js\" data-turbolinks-track=\"reload\"><\/script>\n \n<\/head>\n<body class=\"page-sessions\" data-controller-name=\"sessions\">\n\n <div id=\"welcome-page\" class=\"hide-page\">\n <div \r\nstyle=\"background-image: url(https://testerhome.com/uploads/photo/2019/971bc4ea-b6eb-4d64-b6a1-1e71928314af.jpg); background-position: top center; background-attachment: fixed; background-size: cover; background-repeat: no-repeat; height:100%\">\r\n<\/div>\n <div class=\"welcome-action-button\">\n <div class=\"btn-group\">\n <a id=\"welcome-page-skip\" type=\"button\" class=\"btn btn-primary\">跳过<\/a>\n <\/div>\n <div class=\"btn-group\">\n <a href=\"http://2019.test-china.org/\" type=\"button\" class=\"btn btn-info\">查看详情<\/a>\n <\/div>\n <\/div>\n <\/div>\n\n <div id=\"main-page\">\n<div class=\"header\">\n <nav class=\"navbar navbar-inverse navbar-fixed-top navbar-default\">\n <div class=\"container\">\n <div class=\"navbar-header\" id=\"navbar-header\" data-turbolinks-permanent>\n <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#main-navbar-collapse\">\n <span class=\"sr-only\">Toggle<\/span>\n <i class=\"fa fa-reorder\"><\/i>\n <\/button>\n <a href=\"/\" class=\"navbar-brand\"><b>TesterHome<\/b><\/a>\n\n <\/div>\n <div class=\"collapse navbar-collapse\" id=\"main-navbar-collapse\">\n \n <div id=\"main-nav-menu\">\n <ul class=\"nav navbar-nav\">\n <li class=\"\"><a href=\"/topics\">Topics<\/a><\/li><li class=\"\"><a href=\"/bugs\">Bug Tracker<\/a><\/li><li class=\"\"><a href=\"/questions\">QA<\/a><\/li><li class=\"\"><a href=\"/teams\">Teams<\/a><\/li><li class=\"\"><a href=\"/jobs\">招聘<\/a><\/li><li class=\"\"><a href=\"/wiki\">Wiki<\/a><\/li><li class=\"\"><a href=\"/opensource_projects\">开源项目<span class=\"badge-new\">新<\/span><\/a><\/li>\n \n <\/ul>\n<\/div>\n <\/div>\n <ul class=\"nav user-bar navbar-nav navbar-right\">\n <li><a href=\"/account/sign_up\">Sign Up<\/a><\/li>\n <li><a href=\"/account/sign_in\">Sign In<\/a><\/li>\n<\/ul>\n\n<ul class=\"nav navbar-nav user-bar navbar-right\">\n <li class=\"nav-search hidden-xs hidden-sm hidden-md\">\n <form class=\"navbar-form form-search active\" action=\"/search\" method=\"GET\">\n <div class=\"form-group\">\n <input class=\"form-control\" name=\"q\" type=\"text\" value=\"\" placeholder=\"搜索本站内容\" />\n <\/div>\n <i class=\"fa btn-search fa-search\"><\/i>\n <\/form>\n <\/li>\n\n<\/ul>\n\n <\/div>\n <\/nav>\n <div id=\"corner\" class=\"\">\n <a id=\"cornertip\" href=\"\">\n <div id=\"c-content\">\n <div id=\"c-button\">欢迎<\/div>\n <\/div>\n <\/a>\n <\/div>\n<\/div>\n\n\n\n<div id=\"main\" class=\"main-container container\">\n \n \n \n<div class=\"row\">\n <div class=\"col-md-5 col-md-offset-2\">\n <div class=\"panel panel-default\">\n <div class=\"panel-heading\">Sign In<\/div>\n <div class=\"panel-body\">\n <form class=\"simple_form \" id=\"new_user\" novalidate=\"novalidate\" action=\"/account/sign_in\" accept-charset=\"UTF-8\" data-remote=\"true\" method=\"post\"><input name=\"utf8\" type=\"hidden\" value=\"✓\" />\n <div class=\"form-group\">\n <input type=\"email\" class=\"form-control input-lg\" placeholder=\"用户名 / Email\" name=\"user[login]\" id=\"user_login\" />\n <\/div>\n <div class=\"form-group\">\n <input type=\"password\" class=\"form-control input-lg\" placeholder=\"密码\" name=\"user[password]\" id=\"user_password\" />\n <\/div>\n\n <div class=\"from-group checkbox\">\n <label for=\"user_remember_me\">\n <input name=\"user[remember_me]\" type=\"hidden\" value=\"0\" /><input type=\"checkbox\" value=\"1\" name=\"user[remember_me]\" id=\"user_remember_me\" /> Remember me (2 months)\n <\/label>\n <\/div>\n <div class=\"form-actions\">\n <input type=\"submit\" name=\"commit\" value=\"Sign In\" class=\"btn btn-primary btn-lg btn-block\" data-disable-with=\"Sign In...\" />\n <\/div>\n<\/form> <\/div>\n <\/div>\n <\/div>\n <div class=\"col-md-3\">\n <div class=\"panel panel-default\">\n <div class=\"panel-heading\">Sign in with other services<\/div>\n <ul class=\"list-group\">\n <li class=\"list-group-item\"><a class=\"btn btn-default btn-lg btn-block\" href=\"/account/auth/github\"><i class='fa fa-github'><\/i> GitHub<\/a> <\/li>\n <\/ul>\n <\/div>\n\n <div class=\"panel panel-default\">\n <ul class=\"list-group\">\n\n <li class=\"list-group-item\"><a href=\"/account/sign_up\">Sign Up<\/a><\/li>\n\n <li class=\"list-group-item\"><a href=\"/account/password/new\">Forgot your password?<\/a><\/li>\n\n <li class=\"list-group-item\"><a href=\"/account/confirmation/new\">Havn't received your confirmation email?<\/a><\/li>\n\n <li class=\"list-group-item\"><a href=\"/account/unlock/new\">Havn't received your unlock email?<\/a><\/li>\n<\/ul>\n <\/div>\n <\/div>\n<\/div>\n\n<script>\n $('#new_user').on('ajax:error', function(event, xhr, status, error) {\n App.alert(xhr.responseText, '#main');\n })\n<\/script>\n\n<\/div>\n\n <footer class=\"footer\" id=\"footer\" data-turbolinks-permanent>\n <div class=\"container\">\n \r\n\r\n<div class=\"row\">\r\n <div class=\"col-sm-9\">\r\n <div class=\"media\">\r\n <div class=\"media-left\">\r\n <img src=\"https://testerhome.com/uploads/photo/2016/274e7ebc8ad0db0b7e718fceea3628a9.png!large\" style=\"width:48px;\">\r\n <\/div>\r\n <div class=\"media-body\">\r\n\r\n <div class=\"links\">\r\n <a href=\"https://testerhome.com/wiki/about\">关于<\/a> / \r\n <a href=\"https://testerhome.com/users\">活跃用户<\/a> / \r\n <a href=\"http://test-china.org/\" target=\"_blank\">中国移动互联网测试技术大会<\/a> / \r\n <a href=\"/topics/node13\">反馈<\/a> / \r\n <a href=\"https://github.com/testerhome\">Github<\/a> / \r\n <a href=\"https://testerhome.com//api-doc/\">API<\/a> / \r\n <a href=\"/wiki/spreadtesterhome\">帮助推广<\/a>\r\n\r\n <\/div>\r\n <div class=\"copyright\" style=\"font-size:14px; color:#9CA4A9;margin-top:0px;margin-bottom:5px\">\r\n TesterHome 移动测试社区,由众多移动测试工作者维护,致力于推进国内测试技术。Inspired by RubyChina\r\n <\/div>\r\n<div class=\"links\" data-no-turbolink=\"\">\r\n<span style=\"font-size:14px; color:#666;\">友情链接<\/span><span style=\"margin-left:5px\">\r\n <a style=\"color:#317DDA;\" href=\"http://wetest.qq.com/?from=links_testerhome\" target=\"_blank\">WeTest腾讯质量开放平台<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://www.infoq.com/cn\" target=\"_blank\">InfoQ<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://www.testtao.com/portal.php\" target=\"_blank\">测试之道<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"https://www.testwo.com/\" target=\"_blank\">测试窝<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://tieba.baidu.com/f?ie=utf-8&kw=%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95&fr=search\" target=\"_blank\">百度测试吧<\/a> /\r\n <a style=\"color:#317DDA;\" href=\"http://www.itdks.com/\" target=\"_blank\">IT大咖说<\/a>\r\n<\/span>\r\n <\/div>\r\n <div class=\"links\" style=\"margin-top:0px\" data-no-turbolink=\"\">\r\n <a href=\"?locale=zh-CN\" rel=\"nofollow\">简体中文<\/a> / <a href=\"?locale=zh-TW\" rel=\"nofollow\">正體中文<\/a> / <a href=\"?locale=en\"\r\n rel=\"nofollow\">English<\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n <div class=\"col-sm-3 friends\">\r\n <a href=\"http://www.ucloud.cn/?utm_source=zanzhu&utm_campaign=testerhome&utm_medium=display&utm_content=yejiao&ytag=testerhome_logo\"\r\n target=\"_blank\" rel=\"twipsy\" style=\"display:inline-block;margin-right:5px;\" data-original-title=\"本站服务器由 Ucloud 赞助\"><img src=\"https://testerhome.com/photo/2016/4ce97c93f9433f654884c4839408327a.png\" style=\"height:28px\"><\/a>\r\n\r\n <a href=\"http://www.sendcloud.net/\"\r\n target=\"_blank\" rel=\"twipsy\" style=\"display:inline-block;margin-right:5px;\" data-original-title=\"邮件服务由 SendCloud 赞助\"><img src=\"https://testerhome.com/uploads/photo/2017/0f9fb5db-2472-4430-9f7e-6c051f28c3c9.png\" style=\"height:28px\"><\/a>\r\n\r\n <\/div>\r\n<\/div>\r\n\r\n\r\n\n <\/div>\n <\/footer>\n\n<script type=\"text/javascript\" data-turbolinks-eval=\"false\">\n App.root_url = \"https://testerhome.com/\";\n App.asset_url = \"\";\n App.twemoji_url = \"https://twemoji.b0.upaiyun.com/2\";\n App.locale = \"en\";\n<\/script>\n\n<script>\n ga('create', 'UA-45014075-1', 'auto');\n ga('require', 'displayfeatures');\n ga('send', 'pageview');\n<\/script>\n<div class=\"zoom-overlay\"><\/div>\n<\/div>\n<\/body>\n<\/html>\n"},"redirectURL":null,"headersSize":0,"bodySize":4538},"serverIPAddress":"106.75.214.88","cache":{},"timings":{"dns":12,"connect":125,"ssl":91,"send":1,"wait":82,"receive":7}},{"startedDateTime":"2019-04-19T14:14:14.971+08:00","time":83,"request":{"method":"GET","url":"https://testerhome.com/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png","httpVersion":"HTTP/1.1","cookies":[{"name":"_ga","value":"GA1.2.162109905.1516957848"},{"name":"gsScrollPos-3842","value":"0"},{"name":"gsScrollPos-3871","value":""},{"name":"gsScrollPos-1094","value":"0"},{"name":"gsScrollPos-1837","value":"0"},{"name":"gsScrollPos-1993","value":""},{"name":"gsScrollPos-324","value":"0"},{"name":"gsScrollPos-861","value":"0"},{"name":"gsScrollPos-1568","value":"0"},{"name":"gsScrollPos-2909","value":"0"},{"name":"gsScrollPos-2429","value":""},{"name":"gsScrollPos-5380","value":"0"},{"name":"gsScrollPos-5417","value":"0"},{"name":"gsScrollPos-1937750581","value":""},{"name":"gsScrollPos-1937751827","value":"0"},{"name":"gsScrollPos-1937751846","value":"0"},{"name":"gsScrollPos-1937756244","value":"0"},{"name":"gsScrollPos-1937759407","value":"0"},{"name":"gsScrollPos-968","value":"0"},{"name":"gsScrollPos-3085","value":""},{"name":"hasSkipWelcomePage","value":"1"},{"name":"gsScrollPos-1874","value":"0"},{"name":"gsScrollPos-2049","value":"0"},{"name":"gsScrollPos-3406","value":""},{"name":"user_id","value":"NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f"},{"name":"gsScrollPos-891","value":"0"},{"name":"gsScrollPos-931","value":""},{"name":"gsScrollPos-2436","value":"0"},{"name":"_gid","value":"GA1.2.113994526.1555584877"},{"name":"_gat","value":"1"},{"name":"_homeland_session","value":"%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D"}],"headers":[{"name":"Host","value":"testerhome.com"},{"name":"Connection","value":"keep-alive"},{"name":"Pragma","value":"no-cache"},{"name":"Cache-Control","value":"no-cache"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"},{"name":"Accept","value":"image/webp,image/apng,image/*,*/*;q=0.8"},{"name":"Referer","value":"https://testerhome.com/account/sign_in"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Accept-Language","value":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"},{"name":"Cookie","value":"_ga=GA1.2.162109905.1516957848; gsScrollPos-3842=0; gsScrollPos-3871=; gsScrollPos-1094=0; gsScrollPos-1837=0; gsScrollPos-1993=; gsScrollPos-324=0; gsScrollPos-861=0; gsScrollPos-1568=0; gsScrollPos-2909=0; gsScrollPos-2429=; gsScrollPos-5380=0; gsScrollPos-5417=0; gsScrollPos-1937750581=; gsScrollPos-1937751827=0; gsScrollPos-1937751846=0; gsScrollPos-1937756244=0; gsScrollPos-1937759407=0; gsScrollPos-968=0; gsScrollPos-3085=; hasSkipWelcomePage=1; gsScrollPos-1874=0; gsScrollPos-2049=0; gsScrollPos-3406=; user_id=NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f; gsScrollPos-891=0; gsScrollPos-931=; gsScrollPos-2436=0; _gid=GA1.2.113994526.1555584877; _gat=1; _homeland_session=%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D"}],"queryString":[],"headersSize":1591,"bodySize":0},"response":{"_charlesStatus":"COMPLETE","status":200,"statusText":"OK","httpVersion":"HTTP/1.1","cookies":[],"headers":[{"name":"Server","value":"nginx/1.10.2"},{"name":"Date","value":"Fri, 19 Apr 2019 06:14:17 GMT"},{"name":"Content-Type","value":"image/png"},{"name":"Content-Length","value":"15229"},{"name":"Last-Modified","value":"Sat, 19 Nov 2016 15:26:39 GMT"},{"name":"ETag","value":"\"58306f2f-3b7d\""},{"name":"Expires","value":"Sat, 18 Apr 2020 06:14:17 GMT"},{"name":"Cache-Control","value":"max-age=31536000"},{"name":"Cache-Control","value":"public"},{"name":"Accept-Ranges","value":"bytes"},{"name":"Connection","value":"keep-alive"}],"content":{"size":15229,"mimeType":"image/png","text":"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","encoding":"base64"},"redirectURL":null,"headersSize":0,"bodySize":15229},"serverIPAddress":"106.75.214.88","cache":{},"timings":{"dns":-1,"connect":-1,"ssl":-1,"send":1,"wait":36,"receive":46}},{"startedDateTime":"2019-04-19T14:14:27.834+08:00","time":164,"request":{"method":"POST","url":"https://testerhome.com/account/sign_in","httpVersion":"HTTP/1.1","cookies":[{"name":"_ga","value":"GA1.2.162109905.1516957848"},{"name":"gsScrollPos-3842","value":"0"},{"name":"gsScrollPos-3871","value":""},{"name":"gsScrollPos-1094","value":"0"},{"name":"gsScrollPos-1837","value":"0"},{"name":"gsScrollPos-1993","value":""},{"name":"gsScrollPos-324","value":"0"},{"name":"gsScrollPos-861","value":"0"},{"name":"gsScrollPos-1568","value":"0"},{"name":"gsScrollPos-2909","value":"0"},{"name":"gsScrollPos-2429","value":""},{"name":"gsScrollPos-5380","value":"0"},{"name":"gsScrollPos-5417","value":"0"},{"name":"gsScrollPos-1937750581","value":""},{"name":"gsScrollPos-1937751827","value":"0"},{"name":"gsScrollPos-1937751846","value":"0"},{"name":"gsScrollPos-1937756244","value":"0"},{"name":"gsScrollPos-1937759407","value":"0"},{"name":"gsScrollPos-968","value":"0"},{"name":"gsScrollPos-3085","value":""},{"name":"hasSkipWelcomePage","value":"1"},{"name":"gsScrollPos-1874","value":"0"},{"name":"gsScrollPos-2049","value":"0"},{"name":"gsScrollPos-3406","value":""},{"name":"user_id","value":"NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f"},{"name":"gsScrollPos-891","value":"0"},{"name":"gsScrollPos-931","value":""},{"name":"gsScrollPos-2436","value":"0"},{"name":"_gid","value":"GA1.2.113994526.1555584877"},{"name":"_gat","value":"1"},{"name":"_homeland_session","value":"%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D"}],"headers":[{"name":"Host","value":"testerhome.com"},{"name":"Connection","value":"keep-alive"},{"name":"Content-Length","value":"134"},{"name":"Accept","value":"*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},{"name":"Origin","value":"https://testerhome.com"},{"name":"X-CSRF-Token","value":"0zAKFDDPnNI2+Vwq/iwDPR9vo7KWobfNLAye4EaGBTlsSxMzTNf39lLF9z35f5mcROM7JgOP+azBCuDe84G+XA=="},{"name":"X-Requested-With","value":"XMLHttpRequest"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"},{"name":"Content-Type","value":"application/x-www-form-urlencoded; charset=UTF-8"},{"name":"Referer","value":"https://testerhome.com/account/sign_in"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Accept-Language","value":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"},{"name":"Cookie","value":"_ga=GA1.2.162109905.1516957848; gsScrollPos-3842=0; gsScrollPos-3871=; gsScrollPos-1094=0; gsScrollPos-1837=0; gsScrollPos-1993=; gsScrollPos-324=0; gsScrollPos-861=0; gsScrollPos-1568=0; gsScrollPos-2909=0; gsScrollPos-2429=; gsScrollPos-5380=0; gsScrollPos-5417=0; gsScrollPos-1937750581=; gsScrollPos-1937751827=0; gsScrollPos-1937751846=0; gsScrollPos-1937756244=0; gsScrollPos-1937759407=0; gsScrollPos-968=0; gsScrollPos-3085=; hasSkipWelcomePage=1; gsScrollPos-1874=0; gsScrollPos-2049=0; gsScrollPos-3406=; user_id=NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f; gsScrollPos-891=0; gsScrollPos-931=; gsScrollPos-2436=0; _gid=GA1.2.113994526.1555584877; _gat=1; _homeland_session=%2BPqmS9%2B8BS0lpK1Q9Nzg5IkUR3B%2BECd1ApYtz33R4UNx60WgtFBIhojxj7lIlJniLVx2U%2BDTZm3wSK%2F85EjQNzjLlxKDMn%2FCkPR%2F9EOpyIhFV7NxFnrSFOAStblX8680tHPAI7uYY5bcR%2FF%2BuT4tAhM0uIRjMCIuFfSEFEvt%2BUnj7aVIkTzLjmmQMZ6EiiXjgwsseai2BDuRKH8Dhww0i%2FeizFdKEq2Uv1FoouIThFd0kjc9LpGnOIruhyE77h7XZw%2FoNOsGTZvH%2F%2BvLcRzBMbTGJGznvljXNg%3D%3D--NOyNIYFvHUu1Bf4g--ZP94D8QQDbRtiIVLWBidWQ%3D%3D"}],"queryString":[],"postData":{"mimeType":"application/x-www-form-urlencoded; charset=UTF-8","params":[{"name":"utf8","value":"✓"},{"name":"user[login]","value":"debugtalk"},{"name":"user[password]","value":"LongForEver"},{"name":"user[remember_me]","value":"0"},{"name":"user[remember_me]","value":"1"},{"name":"commit","value":"Sign In"}]},"headersSize":1796,"bodySize":134},"response":{"_charlesStatus":"COMPLETE","status":200,"statusText":"OK","httpVersion":"HTTP/1.1","cookies":[{"name":"remember_user_token","value":"eyJfcmFpbHMiOnsibWVzc2FnZSI6IlcxczJNVEE1WFN3aUpESmhKREV3SkRkbEwwWTVTREZXTm1waFREaG1lR1V1ZEVocFJFOGlMQ0l4TlRVMU5qVTBORFk1TGprNU1qUTJNalFpWFE9PSIsImV4cCI6IjIwMTktMDYtMTlUMDY6MTQ6MjkuOTkyWiIsInB1ciI6bnVsbH19--343d68b57a6619d293f944bfbf63fa557e5a0dd8","path":"/","domain":null,"expires":"Wed, 19 Jun 2019 06:14:29 -0000","httpOnly":true,"secure":true,"comment":null,"_maxAge":null},{"name":"_homeland_session","value":"PbiWoS7j4O3tTBlLCp9RoWTpYGmGCCJWcCjXhgTJWqCajO540nkdCTYA9PmiCneuZcJm4L7VYhPxnE%2FsQ1g%2BBPzdkzFkkpHT0FF8Htn8iWolqAiK3VNoeUz5EZEVLlrrSqVOoXLjYvylSMoWK624mba%2F4Fkg3E2uXqq4G1jRISyOAr1RtvMwYEo8U1Vo2TLFNNxDeyVM3IEqXOprE3M1nlP9qpeHPDnmWdWyRoiNo5MGIAkaReJIeoxECjdgts3sqYsqpWGuh0lJjdtIbmXNlBap8vU%2B54X7x6h9%2B0s%3D--KR8JpaHy431CsKRy--ueD9I5%2B%2FH%2BlH5vq1eUq4cg%3D%3D","path":"/","domain":null,"expires":"Thu, 18 Jul 2019 06:14:30 -0000","httpOnly":true,"secure":true,"comment":null,"_maxAge":null}],"headers":[{"name":"Server","value":"nginx/1.10.2"},{"name":"Date","value":"Fri, 19 Apr 2019 06:14:30 GMT"},{"name":"Content-Type","value":"text/javascript; charset=utf-8"},{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Accept-Encoding"},{"name":"X-Frame-Options","value":"SAMEORIGIN"},{"name":"X-XSS-Protection","value":"1; mode=block"},{"name":"X-Content-Type-Options","value":"nosniff"},{"name":"X-Download-Options","value":"noopen"},{"name":"X-Permitted-Cross-Domain-Policies","value":"none"},{"name":"Referrer-Policy","value":"strict-origin-when-cross-origin"},{"name":"Location","value":"https://testerhome.com/"},{"name":"ETag","value":"W/\"d2d52e29771efe4882102d03bcbbc5ff\""},{"name":"Cache-Control","value":"max-age=0, private, must-revalidate"},{"name":"Set-Cookie","value":"remember_user_token=eyJfcmFpbHMiOnsibWVzc2FnZSI6IlcxczJNVEE1WFN3aUpESmhKREV3SkRkbEwwWTVTREZXTm1waFREaG1lR1V1ZEVocFJFOGlMQ0l4TlRVMU5qVTBORFk1TGprNU1qUTJNalFpWFE9PSIsImV4cCI6IjIwMTktMDYtMTlUMDY6MTQ6MjkuOTkyWiIsInB1ciI6bnVsbH19--343d68b57a6619d293f944bfbf63fa557e5a0dd8; path=/; expires=Wed, 19 Jun 2019 06:14:29 -0000; secure; HttpOnly"},{"name":"Set-Cookie","value":"_homeland_session=PbiWoS7j4O3tTBlLCp9RoWTpYGmGCCJWcCjXhgTJWqCajO540nkdCTYA9PmiCneuZcJm4L7VYhPxnE%2FsQ1g%2BBPzdkzFkkpHT0FF8Htn8iWolqAiK3VNoeUz5EZEVLlrrSqVOoXLjYvylSMoWK624mba%2F4Fkg3E2uXqq4G1jRISyOAr1RtvMwYEo8U1Vo2TLFNNxDeyVM3IEqXOprE3M1nlP9qpeHPDnmWdWyRoiNo5MGIAkaReJIeoxECjdgts3sqYsqpWGuh0lJjdtIbmXNlBap8vU%2B54X7x6h9%2B0s%3D--KR8JpaHy431CsKRy--ueD9I5%2B%2FH%2BlH5vq1eUq4cg%3D%3D; path=/; expires=Thu, 18 Jul 2019 06:14:30 -0000; secure; HttpOnly"},{"name":"X-Request-Id","value":"149b5636-67ef-4ea6-825b-6894894c1e97"},{"name":"X-Runtime","value":"0.124799"},{"name":"Strict-Transport-Security","value":"max-age=15552000; includeSubDomains"},{"name":"Content-Encoding","value":"gzip"},{"name":"Connection","value":"keep-alive"}],"content":{"size":89,"mimeType":"text/javascript; charset=utf-8","text":"Turbolinks.clearCache()\nTurbolinks.visit(\"https://testerhome.com/\", {\"action\":\"replace\"})"},"redirectURL":"https://testerhome.com/","headersSize":0,"bodySize":109},"serverIPAddress":"106.75.214.88","cache":{},"timings":{"dns":-1,"connect":-1,"ssl":-1,"send":1,"wait":162,"receive":1}},{"startedDateTime":"2019-04-19T14:14:28.011+08:00","time":158,"request":{"method":"GET","url":"https://testerhome.com/","httpVersion":"HTTP/1.1","cookies":[{"name":"_ga","value":"GA1.2.162109905.1516957848"},{"name":"gsScrollPos-3842","value":"0"},{"name":"gsScrollPos-3871","value":""},{"name":"gsScrollPos-1094","value":"0"},{"name":"gsScrollPos-1837","value":"0"},{"name":"gsScrollPos-1993","value":""},{"name":"gsScrollPos-324","value":"0"},{"name":"gsScrollPos-861","value":"0"},{"name":"gsScrollPos-1568","value":"0"},{"name":"gsScrollPos-2909","value":"0"},{"name":"gsScrollPos-2429","value":""},{"name":"gsScrollPos-5380","value":"0"},{"name":"gsScrollPos-5417","value":"0"},{"name":"gsScrollPos-1937750581","value":""},{"name":"gsScrollPos-1937751827","value":"0"},{"name":"gsScrollPos-1937751846","value":"0"},{"name":"gsScrollPos-1937756244","value":"0"},{"name":"gsScrollPos-1937759407","value":"0"},{"name":"gsScrollPos-968","value":"0"},{"name":"gsScrollPos-3085","value":""},{"name":"hasSkipWelcomePage","value":"1"},{"name":"gsScrollPos-1874","value":"0"},{"name":"gsScrollPos-2049","value":"0"},{"name":"gsScrollPos-3406","value":""},{"name":"user_id","value":"NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f"},{"name":"gsScrollPos-891","value":"0"},{"name":"gsScrollPos-931","value":""},{"name":"gsScrollPos-2436","value":"0"},{"name":"_gid","value":"GA1.2.113994526.1555584877"},{"name":"_gat","value":"1"},{"name":"remember_user_token","value":"eyJfcmFpbHMiOnsibWVzc2FnZSI6IlcxczJNVEE1WFN3aUpESmhKREV3SkRkbEwwWTVTREZXTm1waFREaG1lR1V1ZEVocFJFOGlMQ0l4TlRVMU5qVTBORFk1TGprNU1qUTJNalFpWFE9PSIsImV4cCI6IjIwMTktMDYtMTlUMDY6MTQ6MjkuOTkyWiIsInB1ciI6bnVsbH19--343d68b57a6619d293f944bfbf63fa557e5a0dd8"},{"name":"_homeland_session","value":"PbiWoS7j4O3tTBlLCp9RoWTpYGmGCCJWcCjXhgTJWqCajO540nkdCTYA9PmiCneuZcJm4L7VYhPxnE%2FsQ1g%2BBPzdkzFkkpHT0FF8Htn8iWolqAiK3VNoeUz5EZEVLlrrSqVOoXLjYvylSMoWK624mba%2F4Fkg3E2uXqq4G1jRISyOAr1RtvMwYEo8U1Vo2TLFNNxDeyVM3IEqXOprE3M1nlP9qpeHPDnmWdWyRoiNo5MGIAkaReJIeoxECjdgts3sqYsqpWGuh0lJjdtIbmXNlBap8vU%2B54X7x6h9%2B0s%3D--KR8JpaHy431CsKRy--ueD9I5%2B%2FH%2BlH5vq1eUq4cg%3D%3D"}],"headers":[{"name":"Host","value":"testerhome.com"},{"name":"Connection","value":"keep-alive"},{"name":"Accept","value":"text/html, application/xhtml+xml"},{"name":"Turbolinks-Referrer","value":"https://testerhome.com/account/sign_in"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"},{"name":"Referer","value":"https://testerhome.com/account/sign_in"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Accept-Language","value":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"},{"name":"Cookie","value":"_ga=GA1.2.162109905.1516957848; gsScrollPos-3842=0; gsScrollPos-3871=; gsScrollPos-1094=0; gsScrollPos-1837=0; gsScrollPos-1993=; gsScrollPos-324=0; gsScrollPos-861=0; gsScrollPos-1568=0; gsScrollPos-2909=0; gsScrollPos-2429=; gsScrollPos-5380=0; gsScrollPos-5417=0; gsScrollPos-1937750581=; gsScrollPos-1937751827=0; gsScrollPos-1937751846=0; gsScrollPos-1937756244=0; gsScrollPos-1937759407=0; gsScrollPos-968=0; gsScrollPos-3085=; hasSkipWelcomePage=1; gsScrollPos-1874=0; gsScrollPos-2049=0; gsScrollPos-3406=; user_id=NjEwOQ%3D%3D--a85617b1d508c153c6ac2d40bd49b25a1466988f; gsScrollPos-891=0; gsScrollPos-931=; gsScrollPos-2436=0; _gid=GA1.2.113994526.1555584877; _gat=1; remember_user_token=eyJfcmFpbHMiOnsibWVzc2FnZSI6IlcxczJNVEE1WFN3aUpESmhKREV3SkRkbEwwWTVTREZXTm1waFREaG1lR1V1ZEVocFJFOGlMQ0l4TlRVMU5qVTBORFk1TGprNU1qUTJNalFpWFE9PSIsImV4cCI6IjIwMTktMDYtMTlUMDY6MTQ6MjkuOTkyWiIsInB1ciI6bnVsbH19--343d68b57a6619d293f944bfbf63fa557e5a0dd8; _homeland_session=PbiWoS7j4O3tTBlLCp9RoWTpYGmGCCJWcCjXhgTJWqCajO540nkdCTYA9PmiCneuZcJm4L7VYhPxnE%2FsQ1g%2BBPzdkzFkkpHT0FF8Htn8iWolqAiK3VNoeUz5EZEVLlrrSqVOoXLjYvylSMoWK624mba%2F4Fkg3E2uXqq4G1jRISyOAr1RtvMwYEo8U1Vo2TLFNNxDeyVM3IEqXOprE3M1nlP9qpeHPDnmWdWyRoiNo5MGIAkaReJIeoxECjdgts3sqYsqpWGuh0lJjdtIbmXNlBap8vU%2B54X7x6h9%2B0s%3D--KR8JpaHy431CsKRy--ueD9I5%2B%2FH%2BlH5vq1eUq4cg%3D%3D"},{"name":"If-None-Match","value":"W/\"bad62c68dac27b01151516aad5c7f0be\""}],"queryString":[],"headersSize":1829,"bodySize":0},"response":{"_charlesStatus":"COMPLETE","status":200,"statusText":"OK","httpVersion":"HTTP/1.1","cookies":[{"name":"_homeland_session","value":"2H2EpF3BCnG%2FjK7qt8ZryfK3zYKsTqxtfoXvNeb7oYUNjCHZr3lV2lIhH4D6KZ1SWDEgItreKFMI2f%2FuR8UE5Iy1P9VSXrPhtRuMxGQnshQgAftJ83KwBriYXSA0MdRuqEE1%2B5nJxDNbnN5sxwsyVCQZe6x5GV%2FCice0d4bczcrV744Hngp1licLOa29YPMpHy5CbU4mcLNRxi%2Bx2f%2B%2B86Lw7d16u10Dake2OZNVjksDTGJYxF9jXQzNNqF2ZUSSq202gB2xUjME4ZURDdcrTE6bIp66KULllVOWmHTkvuBtJA3G6DyZhqoD5gZ5OXZYzgY%2FRQm7u1GeJrNRquJKmZ9u6%2FxIiDF%2FxHjZv%2Fb%2FptJQWVwm2MfCFbnR1bKSfQyi3WnrBkylE84Fk5hFQw%3D%3D--B5BuYCY2BWs3sTg%2B--e9ZCJstvL2HPacQ1gd51sQ%3D%3D","path":"/","domain":null,"expires":"Thu, 18 Jul 2019 06:14:30 -0000","httpOnly":true,"secure":true,"comment":null,"_maxAge":null}],"headers":[{"name":"Server","value":"nginx/1.10.2"},{"name":"Date","value":"Fri, 19 Apr 2019 06:14:30 GMT"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Accept-Encoding"},{"name":"X-Frame-Options","value":"SAMEORIGIN"},{"name":"X-XSS-Protection","value":"1; mode=block"},{"name":"X-Content-Type-Options","value":"nosniff"},{"name":"X-Download-Options","value":"noopen"},{"name":"X-Permitted-Cross-Domain-Policies","value":"none"},{"name":"Referrer-Policy","value":"strict-origin-when-cross-origin"},{"name":"ETag","value":"W/\"d5e2bdc2e5b529a1b78123e327656514\""},{"name":"Cache-Control","value":"max-age=0, private, must-revalidate"},{"name":"Content-Security-Policy","value":";"},{"name":"Set-Cookie","value":"_homeland_session=2H2EpF3BCnG%2FjK7qt8ZryfK3zYKsTqxtfoXvNeb7oYUNjCHZr3lV2lIhH4D6KZ1SWDEgItreKFMI2f%2FuR8UE5Iy1P9VSXrPhtRuMxGQnshQgAftJ83KwBriYXSA0MdRuqEE1%2B5nJxDNbnN5sxwsyVCQZe6x5GV%2FCice0d4bczcrV744Hngp1licLOa29YPMpHy5CbU4mcLNRxi%2Bx2f%2B%2B86Lw7d16u10Dake2OZNVjksDTGJYxF9jXQzNNqF2ZUSSq202gB2xUjME4ZURDdcrTE6bIp66KULllVOWmHTkvuBtJA3G6DyZhqoD5gZ5OXZYzgY%2FRQm7u1GeJrNRquJKmZ9u6%2FxIiDF%2FxHjZv%2Fb%2FptJQWVwm2MfCFbnR1bKSfQyi3WnrBkylE84Fk5hFQw%3D%3D--B5BuYCY2BWs3sTg%2B--e9ZCJstvL2HPacQ1gd51sQ%3D%3D; path=/; expires=Thu, 18 Jul 2019 06:14:30 -0000; secure; HttpOnly"},{"name":"X-Request-Id","value":"bfe4bffb-f4f5-4b94-b381-8799f7a029bd"},{"name":"X-Runtime","value":"0.114561"},{"name":"Strict-Transport-Security","value":"max-age=15552000; includeSubDomains"},{"name":"Content-Encoding","value":"gzip"},{"name":"Connection","value":"keep-alive"}],"content":{"size":54096,"compression":42723,"mimeType":"text/html; charset=utf-8","text":"<!--\n _ _ _ _\n | | | | | | | |\n | |_| | ___ _ __ ___ ___| | __ _ _ __ __| |\n | _ |/ _ \\| '_ ` _ \\ / _ \\ |/ _` | '_ \\ / _` |\n | | | | (_) | | | | | | __/ | (_| | | | | (_| |\n \\_| |_/\\___/|_| |_| |_|\\___|_|\\__,_|_| |_|\\__,_|\n ------------------------------------------------\n https://gethomeland.com\n\n - Ruby: 2.4.0-p0\n - Rails: 5.2.0.rc1\n - Homeland: 3.1.0.beta9\n-->\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset='utf-8'/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\"/>\n <meta name=\"keywords\" content=\"移动测试,游戏测试,性能测试,软件测试,软件测试社区,软件测试资料,软件测试工具,软件测试报告,软件测试方法,自动化测试,软件测试招聘,\">\n <meta name=\"description\" content=\"TesterHome软件测试社区,人气最旺的软件测试技术门户,提供软件测试社区交流,测试沙龙。\">\n <title>TesterHome<\/title>\n <link rel=\"icon\" href=\"/assets/favicon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"apple-touch-icon-precomposed\" href=\"/assets/ios-icon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"shortcut icon\" href=\"/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\"/>\n <link rel=\"apple-touch-icon\" href=\"/assets/favicon-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png\">\n\n <!-- http://www.favicon-generator.org/ -->\n <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"/apple-icon-57x57.png\">\n <link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"/apple-icon-60x60.png\">\n <link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"/apple-icon-72x72.png\">\n <link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"/apple-icon-76x76.png\">\n <link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"/apple-icon-114x114.png\">\n <link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"/apple-icon-120x120.png\">\n <link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"/apple-icon-144x144.png\">\n <link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"/apple-icon-152x152.png\">\n <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-icon-180x180.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/android-icon-192x192.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"/favicon-96x96.png\">\n <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n <link rel=\"manifest\" href=\"/manifest.json\">\n <meta name=\"msapplication-TileColor\" content=\"#ffffff\">\n <meta name=\"msapplication-TileImage\" content=\"/ms-icon-144x144.png\">\n <meta name=\"theme-color\" content=\"#ffffff\">\n\n <meta name=\"apple-mobile-web-app-capable\" content=\"no\">\n <meta content='True' name='HandheldFriendly'/>\n <link rel=\"alternate\" type=\"application/rss+xml\" title=\"订阅最新帖\" href=\"https://testerhome.com/topics/feed\"/>\n <link rel=\"stylesheet\" media=\"screen\" href=\"/assets/front-15185e3983cd68677b04329670c6a6b7fedecb51f1b39ff7fac48180c1850eaa.css\" data-turbolinks-track=\"reload\" />\n \n \n <meta name=\"action-cable-url\" content=\"/cable\" />\n <meta name=\"csrf-param\" content=\"authenticity_token\" />\n<meta name=\"csrf-token\" content=\"jmdCmpEL4XJ0dsx9KfGu5m2XI1rDE5wBYn7FmGqzPss2kszANWkmYblmvJtqydpgdWrDQktsHETJYVo+BER5Ww==\" />\n <meta itemprop=\"name\" content=\"TesterHome\"/>\r\n <meta itemprop=\"image\" content=\"//10.url.cn/qqcourse_logo_ng/ajNVdqHZLLAH8YXbXMDFib2SnIqhac60vw3BspyLd0TO82PiaJ2xfbvXynrocic7ajbCGribAic88wgc/\"/>\r\n\r\n<style type=\"text/css\">\r\n\r\n.blog-description{\r\n text-align: left;\r\n padding: 18px;\r\n}\r\n.blog-description:first-letter {\r\nfont-size : 200%;\r\nfont-weight : bold;\r\nfloat : left;\r\nmargin-right: 3px;\r\n}\r\n\r\n/*\r\n.new-topic:before {\r\n content:url(\"https://testerhome.com/uploads/photo/2019/d80c7d7c-7727-45b2-a98d-2c87a68c65e8.png\");\r\n position: absolute;\r\n left:60%;\r\n margin-top:-35px;\r\n}\r\n*/\r\n<\/style>\n <meta name=\"current-user\" data-user-id=\"6109\" data-user-login=\"debugtalk\" data-user-name=\"debugtalk\" data-user-email=\"mail@debugtalk.com\" data-user-avatar-url=\"/uploads/user/avatar/6109.jpg!md\">\n\n <script src=\"/assets/app-04f83676fc1f6160a4410556b3298892a4536d9e8ee6c9b54df9191dd8dd9060.js\" data-turbolinks-track=\"reload\"><\/script>\n \n<\/head>\n<body class=\"page-home\" data-controller-name=\"home\">\n\n <div id=\"welcome-page\" class=\"hide-page\">\n <div \r\nstyle=\"background-image: url(https://testerhome.com/uploads/photo/2019/971bc4ea-b6eb-4d64-b6a1-1e71928314af.jpg); background-position: top center; background-attachment: fixed; background-size: cover; background-repeat: no-repeat; height:100%\">\r\n<\/div>\n <div class=\"welcome-action-button\">\n <div class=\"btn-group\">\n <a id=\"welcome-page-skip\" type=\"button\" class=\"btn btn-primary\">跳过<\/a>\n <\/div>\n <div class=\"btn-group\">\n <a href=\"http://2019.test-china.org/\" type=\"button\" class=\"btn btn-info\">查看详情<\/a>\n <\/div>\n <\/div>\n <\/div>\n\n <div id=\"main-page\">\n<div class=\"header\">\n <nav class=\"navbar navbar-inverse navbar-fixed-top navbar-default\">\n <div class=\"container\">\n <div class=\"navbar-header\" id=\"navbar-header\" data-turbolinks-permanent>\n <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#main-navbar-collapse\">\n <span class=\"sr-only\">Toggle<\/span>\n <i class=\"fa fa-reorder\"><\/i>\n <\/button>\n <a href=\"/\" class=\"navbar-brand\"><b>TesterHome<\/b><\/a>\n\n <\/div>\n <div class=\"collapse navbar-collapse\" id=\"main-navbar-collapse\">\n \n <div id=\"main-nav-menu\">\n <ul class=\"nav navbar-nav\">\n <li class=\"\"><a href=\"/topics\">Topics<\/a><\/li><li class=\"\"><a href=\"/bugs\">Bug Tracker<\/a><\/li><li class=\"\"><a href=\"/questions\">QA<\/a><\/li><li class=\"\"><a href=\"/teams\">Teams<\/a><\/li><li class=\"\"><a href=\"/jobs\">招聘<\/a><\/li><li class=\"\"><a href=\"/wiki\">Wiki<\/a><\/li><li class=\"\"><a href=\"/opensource_projects\">开源项目<span class=\"badge-new\">新<\/span><\/a><\/li>\n \n <\/ul>\n<\/div>\n <\/div>\n <ul class=\"nav user-bar navbar-nav navbar-right\">\n <li class=\"dropdown dropdown-avatar\">\n <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\" role=\"button\" aria-expanded=\"false\">\n <img class=\"media-object avatar-32\" src=\"/uploads/user/avatar/6109.jpg!sm\" /> <span class=\"caret\"><\/span>\n <\/a>\n <button class=\"navbar-toggle\" type=\"button\" data-toggle=\"dropdown\" role=\"button\" aria-expanded=\"false\">\n <span class=\"sr-only\">Toggle<\/span>\n <img class=\"media-object avatar-32\" src=\"/uploads/user/avatar/6109.jpg!sm\" />\n <\/button>\n <ul class=\"dropdown-menu\" role=\"menu\"><li class=\"\"><a href=\"/debugtalk\">debugtalk<\/a><\/li><li class=\"\"><div class='divider'><\/div><\/li><li class=\"\"><a href=\"/setting\">Account Profile<\/a><\/li><li class=\"\"><a href=\"/debugtalk/columns\">我的专栏<\/a><\/li><li class=\"\"><a href=\"/topics/favorites\">Favorites<\/a><\/li><li class=\"\"><a href=\"/notes\">记事本<\/a><\/li><li class=\"\"><div class='divider'><\/div><\/li><li class=\"\"><a rel=\"nofollow\" data-method=\"delete\" href=\"/account/sign_out\">Sign Out<\/a><\/li><\/ul>\n <\/li>\n<\/ul>\n\n<ul class=\"nav navbar-nav user-bar navbar-right\">\n <li class=\"nav-search hidden-xs hidden-sm hidden-md\">\n <form class=\"navbar-form form-search active\" action=\"/search\" method=\"GET\">\n <div class=\"form-group\">\n <input class=\"form-control\" name=\"q\" type=\"text\" value=\"\" placeholder=\"搜索本站内容\" />\n <\/div>\n <i class=\"fa btn-search fa-search\"><\/i>\n <\/form>\n <\/li>\n\n\n <li class=\"visible-xs\">\n <a href=\"/topics/new\" title=\"New Topic\"><i class=\"fa fa-edit\"><\/i><\/a>\n <\/li>\n\n <li class=\"notification-count hidden-sm\">\n <a href=\"/notifications\" class=\"\" title=\"通知\"><i class=\"fa fa-bell\"><\/i><span class=\"count\">0<\/span><\/a>\n <\/li>\n <li class=\"hidden-sm\">\n <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\" role=\"button\" aria-expanded=\"false\">\n <i class=\"fa fa-plus\"><\/i> <span class=\"caret\"><\/span>\n <\/a>\n <ul class=\"dropdown-menu\" role=\"menu\"><li class=\"\"><a href=\"/topics/new\">New Topic<\/a><\/li><li class=\"\"><div class='divider'><\/div><\/li><li class=\"\"><a href=\"/teams/new\">New team<\/a><\/li><\/ul>\n <\/li>\n<\/ul>\n\n <\/div>\n <\/nav>\n <div id=\"corner\" class=\"\">\n <a id=\"cornertip\" href=\"\">\n <div id=\"c-content\">\n <div id=\"c-button\">欢迎<\/div>\n <\/div>\n <\/a>\n <\/div>\n<\/div>\n\n\n\n<div id=\"main\" class=\"main-container container\">\n \n \n \n\n\n<div class=\"row\">\n <div class=\"col-md-9 home-main\">\n \n \n<div class=\"home_suggest_topics panel panel-default\">\n <div class=\"panel-heading\">社区置顶<\/div>\n <div class=\"panel-body topics row\">\n <div class=\"col-md-6\">\n <div class=\"topic media topic-18791\">\n <div class=\"avatar media-left\">\n <a title=\"kasi (卡斯)\" href=\"/kasi\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/441/ee76af.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"TesterHome 深圳沙龙议题征集\" href=\"/topics/18791\">TesterHome 深圳沙龙议题征集<\/a>\n <i class=\"fa fa-thumb-tack\" title=\"置顶\"><\/i>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"卡斯\" title=\"卡斯(kasi)\" href=\"/kasi\">卡斯<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <\/div>\n<\/div>\n<div class=\"topic media topic-18682\">\n <div class=\"avatar media-left\">\n <a title=\"chenhengjie123 (陈恒捷)\" href=\"/chenhengjie123\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/605.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"TesterHome 广州沙龙 2019年 第 1 期报名中!\" href=\"/topics/18682\">TesterHome 广州沙龙 2019年 第 1 期报名中!<\/a>\n <i class=\"fa fa-thumb-tack\" title=\"置顶\"><\/i>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"陈恒捷\" title=\"陈恒捷(chenhengjie123)\" href=\"/chenhengjie123\">陈恒捷<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18682#reply-141543\">8<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"col-md-6\">\n <div class=\"topic media topic-18683\">\n <div class=\"avatar media-left\">\n <a title=\"LMD (Raymond)\" href=\"/LMD\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/22711/395fcb.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"QA 最佳实践:大厂如何提升软件质量?|福利\" href=\"/topics/18683\">QA 最佳实践:大厂如何提升软件质量?|福利<\/a>\n <i class=\"fa fa-thumb-tack\" title=\"置顶\"><\/i>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"Raymond\" title=\"Raymond(LMD)\" href=\"/LMD\">Raymond<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18683#reply-141529\">4<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17705\">\n <div class=\"avatar media-left\">\n <a title=\"TesterHome (TesterHome)\" href=\"/TesterHome\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/555/652855.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"2019 中国移动互联网测试大会议题征集\" href=\"/topics/17705\">2019 中国移动互联网测试大会议题征集<\/a>\n <i class=\"fa fa-thumb-tack\" title=\"置顶\"><\/i>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"TesterHome\" title=\"TesterHome(TesterHome)\" href=\"/TesterHome\">TesterHome<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17705#reply-140118\">21<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <\/div>\n<\/div>\n\n\n<div class=\"home_suggest_topics panel panel-default\">\n <div class=\"panel-heading\">\n 最新帖子\n <div class=\"pull-right\">\n <a href=\"/topics/last\">查看更多...<\/a>\n <\/div>\n <\/div>\n <div class=\"panel-body topics row\">\n <div class=\"col-md-6\">\n <div class=\"topic media topic-18830\">\n <div class=\"avatar media-left\">\n <a title=\"debugtalk (debugtalk)\" href=\"/debugtalk\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/6109.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"HttpRunner 的测试用例分层机制 (适用于 2.X)\" href=\"/articles/18830\">HttpRunner 的测试用例分层机制 (适用于 2.X)<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"debugtalk\" title=\"debugtalk(debugtalk)\" href=\"/debugtalk\">debugtalk<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18830#reply-141527\">2<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18825\">\n <div class=\"avatar media-left\">\n <a title=\"fengzhaoxi (dishierjie)\" href=\"/fengzhaoxi\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/41499/2ec04d.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"求助,pytest+allure 报错\" href=\"/topics/18825\">求助,pytest+allure 报错<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"dishierjie\" title=\"dishierjie(fengzhaoxi)\" href=\"/fengzhaoxi\">dishierjie<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18825#reply-141549\">2<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18823\">\n <div class=\"avatar media-left\">\n <a title=\"Anitalisk (llsskkii)\" href=\"/Anitalisk\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/42652/d49ba5.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"[深圳] 字节跳动头条研发团队招聘大量测试开发工程师\" href=\"/topics/18823\">[深圳] 字节跳动头条研发团队招聘大量测试开发工程师<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"llsskkii\" title=\"llsskkii(Anitalisk)\" href=\"/Anitalisk\">llsskkii<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18823#reply-141508\">2<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"col-md-6\">\n <div class=\"topic media topic-18837\">\n <div class=\"avatar media-left\">\n <a title=\"a861357276 (吴先森)\" href=\"/a861357276\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/40667/05fb6a.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"软件产品出了问题,就一定是测试的工作没做足,你怎么看\" href=\"/topics/18837\">软件产品出了问题,就一定是测试的工作没做足,你怎么看<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"吴先森\" title=\"吴先森(a861357276)\" href=\"/a861357276\">吴先森<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18837#reply-141561\">1<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18828\">\n <div class=\"avatar media-left\">\n <a title=\"kasijia (kasijia)\" href=\"/kasijia\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/19929/afdd72.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"[已解决] Appium 启动 iOS 真机 app 后无限重启\" href=\"/topics/18828\">[已解决] Appium 启动 iOS 真机 app 后无限重启<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"kasijia\" title=\"kasijia(kasijia)\" href=\"/kasijia\">kasijia<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18828#reply-141556\">3<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18824\">\n <div class=\"avatar media-left\">\n <a title=\"slowchen (慢慢慢慢热)\" href=\"/slowchen\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/28176/ebf091.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"请教一个 Jenkins 发送邮件带附件的问题\" href=\"/topics/18824\">请教一个 Jenkins 发送邮件带附件的问题<\/a>\n \n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"慢慢慢慢热\" title=\"慢慢慢慢热(slowchen)\" href=\"/slowchen\">慢慢慢慢热<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18824#reply-141528\">7<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <\/div>\n<\/div>\n\n<div class=\"home_suggest_topics panel panel-default\">\n <div class=\"panel-heading\">\n 社区精华帖\n <div class=\"pull-right\">\n <a href=\"/topics/excellent\">查看更多精华帖...<\/a>\n <\/div>\n <\/div>\n <div class=\"panel-body topics row\">\n <div class=\"col-md-6 topics-group\">\n <div class=\"topic media topic-18475\">\n <div class=\"avatar media-left\">\n <a title=\"Never_More (Never_More)\" href=\"/Never_More\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/11427.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"基于模型的测试 (Model-based Testing),希望大家能给一些建议\" href=\"/topics/18475\">基于模型的测试 (Model-based Testing),希望大家能给一些建议<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"Never_More\" title=\"Never_More(Never_More)\" href=\"/Never_More\">Never_More<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18475#reply-141489\">26<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18448\">\n <div class=\"avatar media-left\">\n <a title=\"codeskyblue (codeskyblue)\" href=\"/codeskyblue\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/6853/28cf21.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"iOS 远程真机方案整理\" href=\"/topics/18448\">iOS 远程真机方案整理<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"codeskyblue\" title=\"codeskyblue(codeskyblue)\" href=\"/codeskyblue\">codeskyblue<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18448#reply-139214\">14<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18141\">\n <div class=\"avatar media-left\">\n <a title=\"RealLau (非洲赵子龙)\" href=\"/RealLau\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/18875/0ee193.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"强大的全新 Web UI 测试框架 Cypress - 初尝甜头\" href=\"/topics/18141\">强大的全新 Web UI 测试框架 Cypress - 初尝甜头<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"非洲赵子龙\" title=\"非洲赵子龙(RealLau)\" href=\"/RealLau\">非洲赵子龙<\/a>\n for <a class=\"team-name\" data-name=\"Cypress\" title=\"Cypress(CypressChina)\" href=\"/CypressChina\">Cypress<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18141#reply-141263\">36<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18047\">\n <div class=\"avatar media-left\">\n <a title=\"rodman1985 (江城子)\" href=\"/rodman1985\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/5895.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"一九得九,依旧得酒!-- 我的测试总结和展望\" href=\"/topics/18047\">一九得九,依旧得酒!-- 我的测试总结和展望<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"江城子\" title=\"江城子(rodman1985)\" href=\"/rodman1985\">江城子<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18047#reply-139366\">61<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17764\">\n <div class=\"avatar media-left\">\n <a title=\"debugtalk (debugtalk)\" href=\"/debugtalk\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/6109.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"如何度量测试开发的价值产出?\" href=\"/topics/17764\">如何度量测试开发的价值产出?<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"debugtalk\" title=\"debugtalk(debugtalk)\" href=\"/debugtalk\">debugtalk<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17764#reply-141214\">50<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17646\">\n <div class=\"avatar media-left\">\n <a title=\"simple\" href=\"/simple\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/50.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"[精彩盘点] TesterHome 社区 2018年 度精华帖\" href=\"/topics/17646\">[精彩盘点] TesterHome 社区 2018年 度精华帖<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"simple\" title=\"simple(simple)\" href=\"/simple\">simple<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17646#reply-137269\">29<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17540\">\n <div class=\"avatar media-left\">\n <a title=\"youzancoder (有赞测试团队)\" href=\"/youzancoder\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/38409/c59d23.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"资损防控体系介绍\" href=\"/articles/17540\">资损防控体系介绍<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"有赞测试团队\" title=\"有赞测试团队(youzancoder)\" href=\"/youzancoder\">有赞测试团队<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17540#reply-135391\">6<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17292\">\n <div class=\"avatar media-left\">\n <a title=\"xinxi1990 (xinxi)\" href=\"/xinxi1990\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/26433/ebf4e4.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"使用 uiautomator2+pytest+allure 进行 Android 的 UI 自动化测试\" href=\"/topics/17292\">使用 uiautomator2+pytest+allure 进行 Android 的 UI 自动化测试<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"xinxi\" title=\"xinxi(xinxi1990)\" href=\"/xinxi1990\">xinxi<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17292#reply-139303\">35<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17232\">\n <div class=\"avatar media-left\">\n <a title=\"xinxi1990 (xinxi)\" href=\"/xinxi1990\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/26433/ebf4e4.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"iOS 启动时间测试\" href=\"/topics/17232\">iOS 启动时间测试<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"xinxi\" title=\"xinxi(xinxi1990)\" href=\"/xinxi1990\">xinxi<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17232#reply-138912\">10<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17117\">\n <div class=\"avatar media-left\">\n <a title=\"xinxi1990 (xinxi)\" href=\"/xinxi1990\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/26433/ebf4e4.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"SonarQube 的安装与使用\" href=\"/topics/17117\">SonarQube 的安装与使用<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"xinxi\" title=\"xinxi(xinxi1990)\" href=\"/xinxi1990\">xinxi<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17117#reply-129553\">13<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"col-md-6 topics-group\">\n <div class=\"topic media topic-18494\">\n <div class=\"avatar media-left\">\n <a title=\"hlylearner (大豆子)\" href=\"/hlylearner\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/41005/cb690e.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"深入了解 sonar 自定义规则开发 (入门强烈推荐)\" href=\"/topics/18494\">深入了解 sonar 自定义规则开发 (入门强烈推荐)<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"大豆子\" title=\"大豆子(hlylearner)\" href=\"/hlylearner\">大豆子<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18494#reply-139963\">9<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18460\">\n <div class=\"avatar media-left\">\n <a title=\"fenny.ren (fenny)\" href=\"/fenny.ren\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/38212/6e7237.jpeg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"读阿里《不止代码》\u2014\u2014面试\" href=\"/topics/18460\">读阿里《不止代码》\u2014\u2014面试<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"fenny\" title=\"fenny(fenny.ren)\" href=\"/fenny.ren\">fenny<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18460#reply-141436\">13<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18171\">\n <div class=\"avatar media-left\">\n <a title=\"chenhengjie123 (陈恒捷)\" href=\"/chenhengjie123\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/605.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"谷歌开源模糊测试工具 ClusterFuzz 尝鲜记录 (进行中)\" href=\"/topics/18171\">谷歌开源模糊测试工具 ClusterFuzz 尝鲜记录 (进行中)<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"陈恒捷\" title=\"陈恒捷(chenhengjie123)\" href=\"/chenhengjie123\">陈恒捷<\/a>\n for <a class=\"team-name\" data-name=\"PPmoney\" title=\"PPmoney(ppmoney)\" href=\"/ppmoney\">PPmoney<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18171#reply-141020\">46<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-18064\">\n <div class=\"avatar media-left\">\n <a title=\"debugtalk (debugtalk)\" href=\"/debugtalk\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/6109.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"我的 2018 年终总结\" href=\"/topics/18064\">我的 2018 年终总结<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"debugtalk\" title=\"debugtalk(debugtalk)\" href=\"/debugtalk\">debugtalk<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/18064#reply-138728\">28<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17986\">\n <div class=\"avatar media-left\">\n <a title=\"t880216t (81\u20141)\" href=\"/t880216t\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/6859.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"基于 Jmeter 的 web 端接口自动化测试平台\" href=\"/topics/17986\">基于 Jmeter 的 web 端接口自动化测试平台<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"81\u20141\" title=\"81\u20141(t880216t)\" href=\"/t880216t\">81\u20141<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17986#reply-141340\">75<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17746\">\n <div class=\"avatar media-left\">\n <a title=\"weijb (jb)\" href=\"/weijb\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/13676/f12b84.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"听说安卓微信 7.0 不能抓 https?\" href=\"/articles/17746\">听说安卓微信 7.0 不能抓 https?<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"jb\" title=\"jb(weijb)\" href=\"/weijb\">jb<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17746#reply-141478\">45<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17554\">\n <div class=\"avatar media-left\">\n <a title=\"devin (Devin)\" href=\"/devin\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/3791/69f795.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"(开源) XMind2TestCase:一个高效测试用例设计的解决方案! \" href=\"/topics/17554\">(开源) XMind2TestCase:一个高效测试用例设计的解决方案! <\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"Devin\" title=\"Devin(devin)\" href=\"/devin\">Devin<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17554#reply-141235\">78<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17355\">\n <div class=\"avatar media-left\">\n <a title=\"youzancoder (有赞测试团队)\" href=\"/youzancoder\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/38409/c59d23.png!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"有赞全链路压测实战\" href=\"/articles/17355\">有赞全链路压测实战<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"有赞测试团队\" title=\"有赞测试团队(youzancoder)\" href=\"/youzancoder\">有赞测试团队<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17355#reply-141550\">14<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17251\">\n <div class=\"avatar media-left\">\n <a title=\"cay (蒋刚毅)\" href=\"/cay\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/8985/1522fe.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"[持续交付实践] Jenkins Pipeline 高可用设计方法\" href=\"/topics/17251\">[持续交付实践] Jenkins Pipeline 高可用设计方法<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"蒋刚毅\" title=\"蒋刚毅(cay)\" href=\"/cay\">蒋刚毅<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17251#reply-129650\">7<\/a>\n <\/div>\n<\/div>\n<div class=\"topic media topic-17179\">\n <div class=\"avatar media-left\">\n <a title=\"appetizer.io (AppetizerIO)\" href=\"/appetizer.io\"><img class=\"media-object avatar-48\" src=\"/uploads/user/avatar/11797.jpg!md\" /><\/a>\n <\/div>\n <div class=\"infos media-body\">\n <div class=\"title media-heading\">\n <a title=\"WiFi 无线拉起执行 APP 稳定性 / 压力测试,免 USB 连线\" href=\"/topics/17179\">WiFi 无线拉起执行 APP 稳定性 / 压力测试,免 USB 连线<\/a>\n <i title=\"精华帖\" class=\"fa fa-diamond\" data-toggle=\"tooltip\"><\/i>\n \n <\/div>\n <div class=\"info\">\n <a class=\"user-name\" data-name=\"AppetizerIO\" title=\"AppetizerIO(appetizer.io)\" href=\"/appetizer.io\">AppetizerIO<\/a>\n for <a class=\"team-name\" data-name=\"AppetizerIO\" title=\"AppetizerIO(appetizerio)\" href=\"/appetizerio\">AppetizerIO<\/a>\n <\/div>\n <\/div>\n <div class=\"count media-right\">\n <a class=\"state-false\" href=\"/topics/17179#reply-139462\">19<\/a>\n <\/div>\n<\/div>\n\n <\/div>\n <\/div>\n<\/div>\n\n\n <div class=\"index-locations panel panel-default\">\n <div class=\"panel-heading\">热门节点 <i class=\"fa fa-dot-circle-o\" aria-hidden=\"true\"><\/i><\/div>\n <div class=\"panel-body location-list\" style=\"text-align:center;\">\n <span class=\"name\"><a title=\"Appium\" data-id=\"23\" href=\"/topics/node23\">Appium<\/a><\/span>\n <span class=\"name\"><a title=\"招聘\" data-id=\"19\" href=\"/topics/node19\">招聘<\/a><\/span>\n <span class=\"name\"><a title=\"新手区\" data-id=\"36\" href=\"/topics/node36\">新手区<\/a><\/span>\n <span class=\"name\"><a title=\"接口测试\" data-id=\"62\" href=\"/topics/node62\">接口测试<\/a><\/span>\n <span class=\"name\"><a title=\"Bug 曝光台\" data-id=\"47\" href=\"/topics/node47\">Bug 曝光台<\/a><\/span>\n <span class=\"name\"><a title=\"违规处理区\" data-id=\"55\" href=\"/topics/node55\">违规处理区<\/a><\/span>\n <span class=\"name\"><a title=\"通用技术\" data-id=\"25\" href=\"/topics/node25\">通用技术<\/a><\/span>\n <span class=\"name\"><a title=\"移动测试基础\" data-id=\"33\" href=\"/topics/node33\">移动测试基础<\/a><\/span>\n <span class=\"name\"><a title=\"灌水\" data-id=\"11\" href=\"/topics/node11\">灌水<\/a><\/span>\n <span class=\"name\"><a title=\"自动化工具\" data-id=\"2\" href=\"/topics/node2\">自动化工具<\/a><\/span>\n <span class=\"name\"><a title=\"匿名吐槽\" data-id=\"37\" href=\"/topics/node37\">匿名吐槽<\/a><\/span>\n <span class=\"name\"><a title=\"Macaca\" data-id=\"68\" href=\"/topics/node68\">Macaca<\/a><\/span>\n <span class=\"name\"><a title=\"性能测试工具\" data-id=\"3\" href=\"/topics/node3\">性能测试工具<\/a><\/span>\n <span class=\"name\"><a title=\"Selenium\" data-id=\"73\" href=\"/topics/node73\">Selenium<\/a><\/span>\n <span class=\"name\"><a title=\"其他测试框架\" data-id=\"31\" href=\"/topics/node31\">其他测试框架<\/a><\/span>\n <span class=\"name\"><a title=\"问答\" data-id=\"20\" href=\"/topics/node20\">问答<\/a><\/span>\n <span class=\"name\"><a title=\"职业经验\" data-id=\"12\" href=\"/topics/node12\">职业经验<\/a><\/span>\n <span class=\"name\"><a title=\"活动沙龙\" data-id=\"24\" href=\"/topics/node24\">活动沙龙<\/a><\/span>\n <span class=\"name\"><a title=\"霍格沃兹测试学院\" data-id=\"81\" href=\"/topics/node81\">霍格沃兹测试学院<\/a><\/span>\n <span class=\"name\"><a title=\"移动性能测试\" data-id=\"32\" href=\"/topics/node32\">移动性能测试<\/a><\/span>\n <span class=\"name\"><a title=\"持续集成\" data-id=\"46\" href=\"/topics/node46\">持续集成<\/a><\/span>\n <span class=\"name\"><a title=\"Robotium\" data-id=\"39\" href=\"/topics/node39\">Robotium<\/a><\/span>\n <span class=\"name\"><a title=\"iOS 测试\" data-id=\"51\" href=\"/topics/node51\">iOS 测试<\/a><\/span>\n <span class=\"name\"><a title=\"UiAutomator\" data-id=\"53\" href=\"/topics/node53\">UiAutomator<\/a><\/span>\n <span class=\"name\"><a title=\"AI测试\" data-id=\"134\" href=\"/topics/node134\">AI测试<\/a><\/span>\n <span class=\"name\"><a title=\"测试管理\" data-id=\"44\" href=\"/topics/node44\">测试管理<\/a><\/span>\n <span class=\"name\"><a title=\"STF\" data-id=\"137\" href=\"/topics/node137\">STF<\/a><\/span>\n <span class=\"name\"><a title=\"社区管理\" data-id=\"13\" href=\"/topics/node13\">社区管理<\/a><\/span>\n <span class=\"name\"><a title=\"WeTest腾讯质量开发平台\" data-id=\"80\" href=\"/topics/node80\">WeTest腾讯质量开发平台<\/a><\/span>\n <span class=\"name\"><a title=\"树洞\" data-id=\"132\" href=\"/topics/node132\">树洞<\/a><\/span>\n <span class=\"name\"><a title=\"前端测试\" data-id=\"16\" href=\"/topics/node16\">前端测试<\/a><\/span>\n <span class=\"name\"><a title=\"fir.im\" data-id=\"49\" href=\"/topics/node49\">fir.im<\/a><\/span>\n <span class=\"name\"><a title=\"ATX\" data-id=\"78\" href=\"/topics/node78\">ATX<\/a><\/span>\n <span class=\"name\"><a title=\"Docker\" data-id=\"48\" href=\"/topics/node48\">Docker<\/a><\/span>\n <span class=\"name\"><a title=\"Linux\" data-id=\"65\" href=\"/topics/node65\">Linux<\/a><\/span>\n <\/div>\n <\/div>\n\n\n <div class=\"index-locations panel panel-default\">\n <div class=\"panel-heading\">Popular locations <i class=\"fa fa-map-marker\"><\/i> <\/div>\n <div class=\"panel-body location-list\" style=\"text-align:center;\">\n <span class=\"name\"><a href=\"/users/city/%E5%8C%97%E4%BA%AC\">北京<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E4%B8%8A%E6%B5%B7\">上海<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E6%B7%B1%E5%9C%B3\">深圳<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E6%9D%AD%E5%B7%9E\">杭州<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E5%B9%BF%E5%B7%9E\">广州<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E6%88%90%E9%83%BD\">成都<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E5%8D%97%E4%BA%AC\">南京<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E8%A5%BF%E5%AE%89\">西安<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E6%AD%A6%E6%B1%89\">武汉<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E8%8B%8F%E5%B7%9E\">苏州<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E5%8E%A6%E9%97%A8\">厦门<\/a><\/span>\n <span class=\"name\"><a href=\"/users/city/%E9%87%8D%E5%BA%86\">重庆<\/a><\/span>\n <\/div>\n <\/div>\n\n \r\n\r\n<div class=\"row home-icons hidden-sm hidden-xs\">\r\n <div class=\"col-md-3\">\r\n <div class=\"item item5\">\r\n <div class=\"icon\">\r\n <img src=\"https://testerhome.com/weixin.jpg\" width=\"100%\" height=\"100%\">\r\n <\/div>\r\n <div class=\"text\">\r\n <a href=\"/topics/popular\">公众号<i class=\"pull-right fa fa-arrow-right\"><\/i><\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n <div class=\"col-md-3\">\r\n <div class=\"item item6\">\r\n <div class=\"icon\">\r\n <img src=\"/photo/2016/45740b8b1487cc191b585bc2da978f06.jpeg\" width=\"100%\" height=\"100%\">\r\n <\/div>\r\n <div class=\"text\">\r\n <a href=\"/topics/popular\">个人号助手<i class=\"pull-right fa fa-arrow-right\"><\/i><\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n <div class=\"col-md-3\">\r\n <div class=\"item item6\">\r\n <div class=\"icon\">\r\n <img src=\"https://testerhome.com/uploads/photo/2017/94db8410-69e5-4cfa-b4c7-66d8783f2bc5.png!large\" width=\"100%\" height=\"100%\">\r\n <\/div>\r\n <div class=\"text\">\r\n <a href=\"https://fir.im/w2j5\">Android 客户端<i class=\"pull-right fa fa-arrow-right\"><\/i><\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n <div class=\"col-md-3\">\r\n <div class=\"item item6\">\r\n <div class=\"icon\">\r\n <img src=\"https://testerhome.com/uploads/photo/2016/bcff0e5a77254c32d9f35760dbc9b369.png!large\" width=\"100%\" height=\"100%\">\r\n <\/div>\r\n <div class=\"text\">\r\n <a href=\"https://itunes.apple.com/us/app/testerhome-guan-fang-ke-hu/id1182812600?ls=1&mt=8\">iOS 客户端<i class=\"pull-right fa fa-arrow-right\"><\/i><\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n\r\n\r\n\r\n<\/div>\n <\/div>\n <div class=\"col-md-3 home-side-bar\">\n \n\n <div class=\"home_suggest_topics panel panel-default\">\n <div class=\"panel-heading\">七日最热 Top10<\/div>\n <div class=\"panel-body\">\n <div class=\"sidebar_topic topic-18785\">\n <div class=\"title\">\n <span class=\"label label-default top3\">1<\/span><\/a>\n\n <a title=\"一道有魔性的面试题 (来自今日头条)\" href=\"/topics/18785\">\n 一道有魔性的面试题 (来自今日头条)\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18775\">\n <div class=\"title\">\n <span class=\"label label-default top3\">2<\/span><\/a>\n\n <a title=\"社会萌新 想问一下不能长期做手工测试的原因\" href=\"/topics/18775\">\n 社会萌新 想问一下不能长期做手工测试的原因\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18813\">\n <div class=\"title\">\n <span class=\"label label-default top3\">3<\/span><\/a>\n\n <a title=\"请问大家自动化时数据依赖的解决方式?感谢\" href=\"/topics/18813\">\n 请问大家自动化时数据依赖的解决方式?感谢\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18798\">\n <div class=\"title\">\n <span class=\"label label-default\">4<\/span><\/a>\n\n <a title=\"乐信集团 (原分期乐) 招人啦,各位大大们快到碗里来\" href=\"/topics/18798\">\n 乐信集团 (原分期乐) 招人啦,各位大大们快到碗里来\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18824\">\n <div class=\"title\">\n <span class=\"label label-default\">5<\/span><\/a>\n\n <a title=\"请教一个 Jenkins 发送邮件带附件的问题\" href=\"/topics/18824\">\n 请教一个 Jenkins 发送邮件带附件的问题\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18777\">\n <div class=\"title\">\n <span class=\"label label-default\">6<\/span><\/a>\n\n <a title=\"有一个疑惑想问大家\" href=\"/topics/18777\">\n 有一个疑惑想问大家\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18750\">\n <div class=\"title\">\n <span class=\"label label-default\">7<\/span><\/a>\n\n <a title=\"修改网页 JS 解除 某资格网 登录 60s 限制问题\" href=\"/topics/18750\">\n 修改网页 JS 解除 某资格网 登录 60s 限制问题\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18789\">\n <div class=\"title\">\n <span class=\"label label-default\">8<\/span><\/a>\n\n <a title=\"[求助] Battery Historian 工具 网页加载报错 无法上传日志文件 推测是 historian-optimized.js 缺失\" href=\"/topics/18789\">\n [求助] Battery Historian 工具 网页加载报错 无法上传日志文件 推测是 historian-optimized.js 缺失\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18770\">\n <div class=\"title\">\n <span class=\"label label-default\">9<\/span><\/a>\n\n <a title=\"这几天很火的套路招聘图片\u2026\u2026\" href=\"/topics/18770\">\n 这几天很火的套路招聘图片\u2026\u2026\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18752\">\n <div class=\"title\">\n <span class=\"label label-default\">10<\/span><\/a>\n\n <a title=\"记一次基于 Robotium 改造的测试实践\" href=\"/topics/18752\">\n 记一次基于 Robotium 改造的测试实践\n<\/a> <\/div>\n<\/div>\n\n <\/div>\n <\/div>\n\n <div class=\"home_suggest_topics panel panel-default\">\n <div class=\"panel-heading\">最新 bug <i class=\"fa fa-bug\" aria-hidden=\"true\"><\/i><\/div>\n <div class=\"panel-body\">\n <div class=\"sidebar_topic topic-18838\">\n <div class=\"title\">\n <span class=\"label label-default top3\">1<\/span><\/a>\n\n <a title=\"使用微信共享 iphone 的语音备忘录,出现 \u201c发生异常,无法分享\u201d 的提示弹框。企业微信可以正常分享。\" href=\"/topics/18838\">\n 使用微信共享 iphone 的语音备忘录,出现 \u201c发生异常,无法分享\u201d 的提示弹框。企业微信可以正常分享。\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18827\">\n <div class=\"title\">\n <span class=\"label label-default top3\">2<\/span><\/a>\n\n <a title=\"马蜂窝的一个小 bug\" href=\"/topics/18827\">\n 马蜂窝的一个小 bug\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18803\">\n <div class=\"title\">\n <span class=\"label label-default top3\">3<\/span><\/a>\n\n <a title=\"阿里旺旺的一个 bug\" href=\"/topics/18803\">\n 阿里旺旺的一个 bug\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18800\">\n <div class=\"title\">\n <span class=\"label label-default\">4<\/span><\/a>\n\n <a title=\"testerhome 手机端 app bug\" href=\"/topics/18800\">\n testerhome 手机端 app bug\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-18764\">\n <div class=\"title\">\n <span class=\"label label-default\">5<\/span><\/a>\n\n <a title=\"mongodb 数据库官网出现的 bug\" href=\"/topics/18764\">\n mongodb 数据库官网出现的 bug\n<\/a> <\/div>\n<\/div>\n\n <\/div>\n <\/div>\n\n<div class=\"home_suggest_topics panel panel-default\">\n<div class=\"panel-heading\">最新收录开源测试项目<\/div>\n<div class=\"panel-body\">\n <div class=\"sidebar_topic topic-72\">\n <div class=\"title\">\n <img class=\"avatar-small\" src=\"/uploads/opensource_project/2019/2ee30f36-7a18-4799-95f9-284a8a5b448c.png\">\n <a title=\"基于图像识别的UI自动化解决方案-FITCH\" href=\"/opensource_projects/https---github-com-williamfzc-fitch\">\n 基于图像识别的UI自动化解决方案-FITCH\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-67\">\n <div class=\"title\">\n <img class=\"avatar-small\" src=\"/uploads/opensource_project/2019/47ca5738-f46e-4ed1-9888-b8a3d23d99be.png\">\n <a title=\"《 iOS-checkIPA 》ipa文件信息检查工具\" href=\"/opensource_projects/ios-checkipa\">\n 《 iOS-checkIPA 》ipa文件信息检查工具\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-73\">\n <div class=\"title\">\n <img class=\"avatar-small\" src=\"/uploads/opensource_project/2019/6e016e67-df4c-497d-9466-e2c2c65fc091.png\">\n <a title=\"Android APP 小工具测试\u201c利器\u201d\" href=\"/opensource_projects/android-app-testtools\">\n Android APP 小工具测试\u201c利器\u201d\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-71\">\n <div class=\"title\">\n <img class=\"avatar-small\" src=\"/uploads/opensource_project/2019/f329de76-35f3-471c-b598-b386c8ad6109.png\">\n <a title=\"RDebug\" href=\"/opensource_projects/rdebug\">\n RDebug\n<\/a> <\/div>\n<\/div>\n<div class=\"sidebar_topic topic-70\">\n <div class=\"title\">\n <img class=\"avatar-small\" src=\"/uploads/opensource_project/2019/37f745ce-7406-49ce-bb79-30eea20233d4.png\">\n <a title=\"HAF\" href=\"/opensource_projects/haf\">\n HAF\n<\/a> <\/div>\n<\/div>\n\n<\/div>\n<\/div>\n\n\n<div id=\"hall-of-fames\" class=\"panel panel-default\">\n <div class=\"panel-heading\">最新加入 <i class='fa fa-fire'><\/i>(新同学)<\/div>\n <div class=\"panel-body\">\n <div class=\"users-label\">\n <a class=\"users-label-item\" href=\"/hulcwater\" title=\"\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42774/915ed5.jpg!sm\">\n hulcwater\n <\/a>\n <a class=\"users-label-item\" href=\"/Simonluepang\" title=\"Shenwei Xu\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42771/634f68.jpg!sm\">\n Simonluepang\n <\/a>\n <a class=\"users-label-item\" href=\"/iamlonghan\" title=\"longhan\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42762/0f1bcc.jpg!sm\">\n iamlonghan\n <\/a>\n <a class=\"users-label-item\" href=\"/HondaXX\" title=\"\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42760/58cb18.jpg!sm\">\n HondaXX\n <\/a>\n <a class=\"users-label-item\" href=\"/ttsiii\" title=\"天都\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42759/bfd30a.jpg!sm\">\n ttsiii\n <\/a>\n <a class=\"users-label-item\" href=\"/DDDDanny\" title=\"TDanny\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42756/ef97fc.jpg!sm\">\n DDDDanny\n <\/a>\n <a class=\"users-label-item\" href=\"/yhx0326\" title=\"yhx\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42754/8c6f19.png!sm\">\n yhx0326\n <\/a>\n <a class=\"users-label-item\" href=\"/zhl12345678\" title=\"\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42750/f8b97a.jpg!sm\">\n zhl12345678\n <\/a>\n <a class=\"users-label-item\" href=\"/shuaimingming\" title=\"帅洺洺\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42746/53cbf6.jpg!sm\">\n shuaimingming\n <\/a>\n <a class=\"users-label-item\" href=\"/todayno\" title=\"今天不打烊\">\n <img class=\"avatar-small inline-block\" src=\"/uploads/user/avatar/42743/04e73c.jpg!sm\">\n todayno\n <\/a>\n <\/div>\n\n <\/div>\n<\/div>\n\n\n\n <div class=\"panel panel-default\">\n <div class=\"panel-heading\">Statistics<\/div>\n <ul class=\"list-group\">\n <li class=\"list-group-item\">社区会员: 37899 人<\/li>\n <li class=\"list-group-item\">帖子数: 18702 个<\/li>\n <li class=\"list-group-item\">回帖数: 141051 条<\/li>\n <\/ul>\n <\/div>\n\n <\/div>\n<\/div>\n<\/div>\n\n <footer class=\"footer\" id=\"footer\" data-turbolinks-permanent>\n <div class=\"container\">\n \r\n\r\n<div class=\"row\">\r\n <div class=\"col-sm-9\">\r\n <div class=\"media\">\r\n <div class=\"media-left\">\r\n <img src=\"https://testerhome.com/uploads/photo/2016/274e7ebc8ad0db0b7e718fceea3628a9.png!large\" style=\"width:48px;\">\r\n <\/div>\r\n <div class=\"media-body\">\r\n\r\n <div class=\"links\">\r\n <a href=\"https://testerhome.com/wiki/about\">关于<\/a> / \r\n <a href=\"https://testerhome.com/users\">活跃用户<\/a> / \r\n <a href=\"http://test-china.org/\" target=\"_blank\">中国移动互联网测试技术大会<\/a> / \r\n <a href=\"/topics/node13\">反馈<\/a> / \r\n <a href=\"https://github.com/testerhome\">Github<\/a> / \r\n <a href=\"https://testerhome.com//api-doc/\">API<\/a> / \r\n <a href=\"/wiki/spreadtesterhome\">帮助推广<\/a>\r\n\r\n <\/div>\r\n <div class=\"copyright\" style=\"font-size:14px; color:#9CA4A9;margin-top:0px;margin-bottom:5px\">\r\n TesterHome 移动测试社区,由众多移动测试工作者维护,致力于推进国内测试技术。Inspired by RubyChina\r\n <\/div>\r\n<div class=\"links\" data-no-turbolink=\"\">\r\n<span style=\"font-size:14px; color:#666;\">友情链接<\/span><span style=\"margin-left:5px\">\r\n <a style=\"color:#317DDA;\" href=\"http://wetest.qq.com/?from=links_testerhome\" target=\"_blank\">WeTest腾讯质量开放平台<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://www.infoq.com/cn\" target=\"_blank\">InfoQ<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://www.testtao.com/portal.php\" target=\"_blank\">测试之道<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"https://www.testwo.com/\" target=\"_blank\">测试窝<\/a> / \r\n <a style=\"color:#317DDA;\" href=\"http://tieba.baidu.com/f?ie=utf-8&kw=%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95&fr=search\" target=\"_blank\">百度测试吧<\/a> /\r\n <a style=\"color:#317DDA;\" href=\"http://www.itdks.com/\" target=\"_blank\">IT大咖说<\/a>\r\n<\/span>\r\n <\/div>\r\n <div class=\"links\" style=\"margin-top:0px\" data-no-turbolink=\"\">\r\n <a href=\"?locale=zh-CN\" rel=\"nofollow\">简体中文<\/a> / <a href=\"?locale=zh-TW\" rel=\"nofollow\">正體中文<\/a> / <a href=\"?locale=en\"\r\n rel=\"nofollow\">English<\/a>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n <\/div>\r\n <div class=\"col-sm-3 friends\">\r\n <a href=\"http://www.ucloud.cn/?utm_source=zanzhu&utm_campaign=testerhome&utm_medium=display&utm_content=yejiao&ytag=testerhome_logo\"\r\n target=\"_blank\" rel=\"twipsy\" style=\"display:inline-block;margin-right:5px;\" data-original-title=\"本站服务器由 Ucloud 赞助\"><img src=\"https://testerhome.com/photo/2016/4ce97c93f9433f654884c4839408327a.png\" style=\"height:28px\"><\/a>\r\n\r\n <a href=\"http://www.sendcloud.net/\"\r\n target=\"_blank\" rel=\"twipsy\" style=\"display:inline-block;margin-right:5px;\" data-original-title=\"邮件服务由 SendCloud 赞助\"><img src=\"https://testerhome.com/uploads/photo/2017/0f9fb5db-2472-4430-9f7e-6c051f28c3c9.png\" style=\"height:28px\"><\/a>\r\n\r\n <\/div>\r\n<\/div>\r\n\r\n\r\n\n <\/div>\n <\/footer>\n\n<script type=\"text/javascript\" data-turbolinks-eval=\"false\">\n App.root_url = \"https://testerhome.com/\";\n App.asset_url = \"\";\n App.twemoji_url = \"https://twemoji.b0.upaiyun.com/2\";\n App.locale = \"en\";\n App.current_user_id = 6109;\n<\/script>\n\n<script>\n ga('create', 'UA-45014075-1', 'auto');\n ga('require', 'displayfeatures');\n ga('send', 'pageview');\n<\/script>\n<div class=\"zoom-overlay\"><\/div>\n<\/div>\n<\/body>\n<\/html>\n"},"redirectURL":null,"headersSize":0,"bodySize":11373},"serverIPAddress":"106.75.214.88","cache":{},"timings":{"dns":-1,"connect":-1,"ssl":-1,"send":1,"wait":152,"receive":5}}]}} \ No newline at end of file diff --git a/docs/data/testerhome-login.yml b/docs/data/testerhome-login.yml new file mode 100644 index 00000000..e24dccf8 --- /dev/null +++ b/docs/data/testerhome-login.yml @@ -0,0 +1,66 @@ +- config: + name: testcase description + variables: {} + verify: False + +- test: + name: /account/sign_in + request: + headers: + If-None-Match: W/"bc9ae267fdcbd89bf1dfaea10dea2b0e" + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/account/sign_in + extract: + X_CSRF_Token: <meta name="csrf-token" content="(.*)" /> + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] + +- test: + name: /assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png + request: + headers: + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, image/png] + +- test: + name: /account/sign_in + request: + data: + commit: Sign In + user[login]: debugtalk + user[password]: XXXXXXXX + user[remember_me]: '1' + utf8: ✓ + headers: + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + X-CSRF-Token: $X_CSRF_Token + X-Requested-With: XMLHttpRequest + method: POST + url: https://testerhome.com/account/sign_in + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/javascript; charset=utf-8] + +- test: + name: / + request: + headers: + If-None-Match: W/"bad62c68dac27b01151516aad5c7f0be" + Turbolinks-Referrer: https://testerhome.com/account/sign_in + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/ + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] diff --git a/docs/data/user_id.csv b/docs/data/user_id.csv new file mode 100644 index 00000000..ae3d9c5f --- /dev/null +++ b/docs/data/user_id.csv @@ -0,0 +1,5 @@ +user_id +1001 +1002 +1003 +1004 diff --git a/docs/development/architecture.md b/docs/development/architecture.md new file mode 100644 index 00000000..f126bc21 --- /dev/null +++ b/docs/development/architecture.md @@ -0,0 +1,2 @@ + +![](../images/HttpRunner-architecture-diagram.svg) diff --git a/docs/development/dev-api.md b/docs/development/dev-api.md new file mode 100644 index 00000000..d1683f96 --- /dev/null +++ b/docs/development/dev-api.md @@ -0,0 +1,342 @@ +# 开发扩展 + +HttpRunner 除了作为命令行工具使用外,还可以作为软件包集成到你自己的项目中。 + +简单来说,HttpRunner 提供了运行 YAML/JSON 格式测试用例的能力,并能返回详细的测试结果信息。 + +## HttpRunner class + +HttpRunner 以 `类(class)` 的形式对外提供调用支持,类名为`HttpRunner`,导入方式如下: + +```python +from httprunner.api import HttpRunner +``` + +### 可用初始化参数 + +`HttpRunner` 内部用于驱动测试执行的是`unittest.TextTestRunner`,在初始化 `HttpRunner` 时可以使用 TextTestRunner 的所有可用参数(详情可阅读[官方文档][TextTestRunner])。除此之外,`HttpRunner`还额外支持一个参数,`http_client_session`,可用于指定不同的客户端类型。 + +通常情况下,初始化 `HttpRunner` 时常用的参数有如下几个: + +- `resultclass`: HtmlTestResult/TextTestResult,默认值为 HtmlTestResult +- `failfast`: 设置为 True 时,测试在首次遇到错误或失败时会停止运行;默认值为 False +- `http_client_session`: 传入`requests.Session()`时进行自动化测试(默认),传入`locust.client.Session()`时进行性能测试 + +例如,如需初始化 `HttpRunner` 时设置 `failfast` 为 False,初始化方式如下所示: + +```python +from httprunner.api import HttpRunner + +runner = HttpRunner(failfast=False) +``` + +### 可用调用方法 + +在 `HttpRunner` 中,对外提供了两个方法: + +- `run`: 运行测试用例 +- `gen_html_report`: 生成 HTML 测试报告 + +### 可用属性 + +在 `HttpRunner` 中,对外提供了一个属性: + +- `summary`: 测试执行结果 + +该属性需要在调用 `run` 方法后获取。 + +## 运行测试用例 + +`HttpRunner` 的 run 方法有三个参数: + +- `path_or_testcases`: 指定要运行的测试用例;支持传入两类参数,YAML/JSON 格式测试用例文件路径,或者标准的测试用例结构体; +- `dot_env_path`(可选): 指定加载环境变量文件(.env)的路径,默认值为当前工作目录(PWD)中的 `.env` 文件 +- `mapping`(可选): 变量映射,可用于对传入测试用例中的变量进行覆盖替换。 + +### 传入测试用例文件路径 + +指定测试用例文件路径支持三种形式: + +- YAML/JSON 文件路径,支持绝对路径和相对路径 +- 包含 YAML/JSON 文件的文件夹,支持绝对路径和相对路径 +- 文件路径和文件夹路径的混合情况(list/set) + +```python +# 文件路径 +runner.run("docs/data/demo-quickstart-2.yml") + +# 文件夹路径 +runner.run("docs/data/") + +# 混合情况 +runner.run(["docs/data/", "files/demo-quickstart-2.yml"]) +``` + +如需指定加载环境变量文件(.env)的路径,或者需要对测试用例中的变量进行覆盖替换,则可使用 `dot_env_path` 和 `mapping` 参数。 + +```python +# dot_env_path +runner.run("docs/data/demo-quickstart-2.yml", dot_env_path="/path/to/.env") + +# mapping +override_mapping = { + "device_sn": "XXX" +} +runner.run("docs/data/demo-quickstart-2.yml", mapping=override_mapping) +``` + +### 传入标准的测试用例结构体 + +除了传入测试用例文件路径,还可以直接传入标准的测试用例结构体。 + +以 [demo-quickstart-2.yml](/data/demo-quickstart-2.yml) 为例,对应的数据结构体如下所示: + +```json +[ + { + "config": { + "name": "testcase description", + "request": { + "base_url": "", + "headers": { + "User-Agent": "python-requests/2.18.4" + } + }, + "variables": [], + "output": ["token"], + "path": "/abs-path/to/demo-quickstart-2.yml", + "refs": { + "env": {}, + "debugtalk": { + "variables": { + "SECRET_KEY": "DebugTalk" + }, + "functions": { + "gen_random_string": <function gen_random_string at 0x108596268>, + "get_sign": <function get_sign at 0x1085962f0>, + "get_user_id": <function get_user_id at 0x108596378>, + "get_account": <function get_account at 0x108596400>, + "get_os_platform": <function get_os_platform at 0x108596488> + } + }, + "def-api": {}, + "def-testcase": {} + } + }, + "teststeps": [ + { + "name": "/api/get-token", + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": {"Content-Type": "application/json", "app_version": "2.8.6", "device_sn": "FwgRiO7CNA50DSU", "os_platform": "ios", "user_agent": "iOS/10.3"}, + "json": {"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"} + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ] + }, + { + "name": "/api/users/1000", + "request": {"url": "http://127.0.0.1:5000/api/users/1000", "method": "POST", "headers": {"Content-Type": "application/json", "device_sn": "FwgRiO7CNA50DSU", "token": "$token"}, + "json": {"name": "user1", "password": "123456"}}, + "validate": [ + {"eq": ["status_code", 201]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.msg", "user created successfully."]} + ] + } + ] + }, + {...} # another testcase +] +``` + +传入测试用例结构体时,支持传入单个结构体(dict),或者多个结构体(list of dict)。 + +```python +# 运行单个结构体 +runner.run(testcase) + +# 运行多个结构体 +runner.run([testcase1, testcase2]) +``` + +### 加载 `debugtalk.py` && `.env` + +通过传入测试用例文件路径运行测试用例时,HttpRunner 会自动以指定测试用例文件路径为起点,向上搜索 `debugtalk.py` 文件,并将 `debugtalk.py` 文件所在的文件目录作为当前工作目录(PWD)。 + +同时,HttpRunner 会在当前工作目录(PWD)下搜索 `.env` 文件,以及 `api` 和 `testcases` 文件夹,并自动进行加载。 + +最终加载得到的存储结构如下所示: + +```json +{ + "env": {}, + "debugtalk": { + "variables": { + "SECRET_KEY": "DebugTalk" + }, + "functions": { + "gen_random_string": <function gen_random_string at 0x108596268>, + "get_sign": <function get_sign at 0x1085962f0>, + "get_user_id": <function get_user_id at 0x108596378>, + "get_account": <function get_account at 0x108596400>, + "get_os_platform": <function get_os_platform at 0x108596488> + } + }, + "def-api": {}, + "def-testcase": {} +} +``` + +其中,`env` 对应的是 `.env` 文件中的环境变量,`debugtalk` 对应的是 `debugtalk.py` 文件中定义的变量和函数,`def-api` 对应的是 `api` 文件夹下定义的接口描述,`def-testcase` 对应的是 `testcases` 文件夹下定义的测试用例。 + +通过传入标准的测试用例结构体执行测试时,传入的数据应包含所有信息,包括 `debugtalk.py`、`.env`、依赖的 api 和 测试用例等;因此也无需再使用 `dot_env_path` 和 `mapping` 参数,所有信息都要通过 `refs` 传入。 + +## 返回详细测试结果数据 + +运行完成后,通过 `summary` 属性可获取详尽的运行结果数据。 + +```python +# get result summary +summary = runner.summary +``` + +其数据结构为: + +```json +{ + "success": true, + "stat": { + "testsRun": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "expectedFailures": 0, + "unexpectedSuccesses": 0, + "successes": 2 + }, + "time": { + "start_at": 1538449655.944404, + "duration": 0.03181314468383789 + }, + "platform": { + "httprunner_version": "1.5.14", + "python_version": "CPython 3.6.5+", + "platform": "Darwin-17.6.0-x86_64-i386-64bit" + }, + "details": [ + { + "success": true, + "name": "testcase description", + "base_url": "", + "stat": {"testsRun": 2, "failures": 0, "errors": 0, "skipped": 0, "expectedFailures": 0, "unexpectedSuccesses": 0, "successes": 2}, + "time": {"start_at": 1538449655.944404, "duration": 0.03181314468383789}, + "records": [ + { + "name": "/api/get-token", + "status": "success", + "attachment": "", + "meta_data": { + "request": { + "url": "http://127.0.0.1:5000/api/get-token", + "method": "POST", + "headers": {"User-Agent": "python-requests/2.18.4", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive", "Content-Type": "application/json", "app_version": "2.8.6", "device_sn": "FwgRiO7CNA50DSU", "os_platform": "ios", "user_agent": "iOS/10.3", "Content-Length": "52"}, + "start_timestamp": 1538449655.944801, + "json": {"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"}, + "body": b'{"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"}' + }, + "response": { + "status_code": 200, + "headers": {"Content-Type": "application/json", "Content-Length": "46", "Server": "Werkzeug/0.14.1 Python/3.6.5+", "Date": "Tue, 02 Oct 2018 03:07:35 GMT"}, + "content_size": 46, + "response_time_ms": 12.87, + "elapsed_ms": 6.955, + "encoding": null, + "content": b'{"success": true, "token": "CcQ7dBjZZbjIXRkG"}', + "content_type": "application/json", + "ok": true, + "url": "http://127.0.0.1:5000/api/get-token", + "reason": "OK", + "cookies": {}, + "text": '{"success": true, "token": "CcQ7dBjZZbjIXRkG"}', + "json": {"success": true, "token": "CcQ7dBjZZbjIXRkG"} + }, + "validators": [ + {"check": "status_code", "expect": 200, "comparator": "eq", "check_value": 200, "check_result": "pass"}, + {"check": "headers.Content-Type", "expect": "application/json", "comparator": "eq", "check_value": "application/json", "check_result": "pass"}, + {"check": "content.success", "expect": true, "comparator": "eq", "check_value": true, "check_result": "pass"} + ] + } + }, + { + "name": "/api/users/1000", + "status": "success", + "attachment": "", + "meta_data": { + "request": { + "url": "http://127.0.0.1:5000/api/users/1000", + "method": "POST", + "headers": {"User-Agent": "python-requests/2.18.4", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive", "Content-Type": "application/json", "device_sn": "FwgRiO7CNA50DSU", "token": "CcQ7dBjZZbjIXRkG", "Content-Length": "39"}, + "start_timestamp": 1538449655.958944, + "json": {"name": "user1", "password": "123456"}, + "body": b'{"name": "user1", "password": "123456"}' + }, + "response": { + "status_code": 201, + "headers": {"Content-Type": "application/json", "Content-Length": "54", "Server": "Werkzeug/0.14.1 Python/3.6.5+", "Date": "Tue, 02 Oct 2018 03:07:35 GMT"}, + "content_size": 54, + "response_time_ms": 3.34, + "elapsed_ms": 2.16, + "encoding": null, + "content": b'{"success": true, "msg": "user created successfully."}', + "content_type": "application/json", + "ok": true, + "url": "http://127.0.0.1:5000/api/users/1000", + "reason": "CREATED", + "cookies": {}, + "text": '{"success": true, "msg": "user created successfully."}', + "json": {"success": true, "msg": "user created successfully."} + }, + "validators": [ + {"check": "status_code", "expect": 201, "comparator": "eq", "check_value": 201, "check_result": "pass"}, + {"check": "headers.Content-Type", "expect": "application/json", "comparator": "eq", "check_value": "application/json", "check_result": "pass"}, + {"check": "content.success", "expect": true, "comparator": "eq", "check_value": true, "check_result": "pass"}, + {"check": "content.msg", "expect": "user created successfully.", "comparator": "eq", "check_value": "user created successfully.", "check_result": "pass"} + ] + } + } + ], + "in_out": { + "in": {"SECRET_KEY": "DebugTalk"}, + "out": {"token": "CcQ7dBjZZbjIXRkG"} + } + } + ] +} +``` + +## 生成 HTML 测试报告 + +如需生成 HTML 测试报告,可调用 `gen_html_report` 方法。 + +```python +# generate html report +runner.gen_html_report( + html_report_name="demo", + html_report_template="/path/to/custom_report_template" +) + +# => reports/demo/demo-1532078874.html +``` + +关于测试报告的详细内容,请查看[测试报告](/run-tests/report/)部分。 + +[TextTestRunner]: https://docs.python.org/3.6/library/unittest.html#unittest.TextTestRunner \ No newline at end of file diff --git a/docs/examples/testerhome-login.md b/docs/examples/testerhome-login.md new file mode 100644 index 00000000..8aec167f --- /dev/null +++ b/docs/examples/testerhome-login.md @@ -0,0 +1,246 @@ + +## 案例介绍 + +通过接口自动化实现 TesterHome 的登录退出功能。 + +![](../images/testerhome-login.png) + +功能描述: + +- 进入[登录页面](https://testerhome.com/account/sign_in) +- 输入账号和密码 +- 点击【Sign In】进行登录 + +## 准备工作 + +### 抓包生成 HAR 文件 + +在浏览器中人工进行登录操作,同时使用抓包工具进行抓包。抓包时建议使用过滤器(Filter),常用的做法是采用被测系统的 host,将无关请求过滤掉。 + +![](../images/testerhome-login-charles.png) + +选择需要生成测试用例的请求,导出为 HTTP Archive (.har) 格式的文件。 + +![](../images/testerhome-charles-export.png) + +![](../images/charles-export-har.png) + +### 转换生成测试用例 + +成功安装 HttpRunner 后,系统中会新增 `har2case` 命令,使用该命令可将 HAR 数据包转换为 HttpRunner 支持的 `YAML/JSON` 测试用例文件。 + +```bash +$ har2case docs/data/testerhome-login.har -2y +INFO:root:Start to generate testcase. +INFO:root:dump testcase to YAML format. +INFO:root:Generate YAML testcase successfully: docs/data/testerhome-login.yml +``` + +生成的测试用例内容如下: + +<details> +<summary>点击查看</summary> + +```yaml +- config: + name: testcase description + variables: {} + +- test: + name: /account/sign_in + request: + headers: + If-None-Match: W/"bc9ae267fdcbd89bf1dfaea10dea2b0e" + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/account/sign_in + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] + +- test: + name: /assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png + request: + headers: + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, image/png] + +- test: + name: /account/sign_in + request: + data: + commit: Sign In + user[login]: debugtalk + user[password]: XXXXXXXX + user[remember_me]: '1' + utf8: ✓ + headers: + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + X-CSRF-Token: 0zAKFDDPnNI2+Vwq/iwDPR9vo7KWobfNLAye4EaGBTlsSxMzTNf39lLF9z35f5mcROM7JgOP+azBCuDe84G+XA== + X-Requested-With: XMLHttpRequest + method: POST + url: https://testerhome.com/account/sign_in + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/javascript; charset=utf-8] + +- test: + name: / + request: + headers: + If-None-Match: W/"bad62c68dac27b01151516aad5c7f0be" + Turbolinks-Referrer: https://testerhome.com/account/sign_in + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/ + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] +``` +</details> + +### 首次运行测试用例 + +成功安装 HttpRunner 后,系统中会新增 `hrun` 命令,该命令是 HttpRunner 的核心命令,用于运行 HttpRunner 支持的 `YAML/JSON` 测试用例文件。 + +生成测试用例后,我们可以先尝试运行一次,大多数情况,如果被测场景中不存在关联的情况,是可以直接运行成功的。 + +```bash +$ hrun docs/data/testerhome-login.yml --failfast --log-level info +INFO Start to run testcase: testcase description +/account/sign_in +INFO GET https://testerhome.com/account/sign_in +INFO status_code: 200, response_time(ms): 189.66 ms, response_length: 12584 bytes + +. +/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png +INFO GET https://testerhome.com/assets/big_logo-cd32144f74c18746f3dce33e1040e7dfe4c07c8e611e37f3868b1c16b5095da3.png +INFO status_code: 200, response_time(ms): 83.98 ms, response_length: 15229 bytes + +. +/account/sign_in +INFO POST https://testerhome.com/account/sign_in +INFO status_code: 200, response_time(ms): 172.8 ms, response_length: 89 bytes + +. +/ +INFO GET https://testerhome.com/ +INFO status_code: 200, response_time(ms): 257.41 ms, response_length: 52463 bytes + +. + +---------------------------------------------------------------------- +Ran 4 tests in 0.722s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1555662601.html +``` + +比较幸运,脚本在没有做任何修改的情况下运行成功了。 + + +## 调试 & 优化测试用例 + +虽然脚本运行成功了,但是为了更好地管理和维护脚本,需要对脚本进行优化调整。 + +### 关联处理 + +查看录制生成的脚本,可以看到在发起登录请求时包含了 `X-CSRF-Token`,如果熟悉网络信息安全的基础知识,就会联想到该字段是动态变化的,每次都是先从服务器端返回至客户端,客户端在后续发起请求时需要携带该字段。 + +```yaml +- test: + name: /account/sign_in + request: + data: + commit: Sign In + user[login]: debugtalk + user[password]: XXXXXXXX + user[remember_me]: '1' + utf8: ✓ + headers: + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + X-CSRF-Token: 0zAKFDDPnNI2+Vwq/iwDPR9vo7KWobfNLAye4EaGBTlsSxMzTNf39lLF9z35f5mcROM7JgOP+azBCuDe84G+XA== + X-Requested-With: XMLHttpRequest + method: POST + url: https://testerhome.com/account/sign_in + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] +``` + +虽然当前直接运行录制生成的脚本也是成功的,但很有可能在过了一段时间后,`X-CSRF-Token` 失效,脚本也就无法再成功运行了。因此在测试脚本中,该字段不能写死为抓包时获取的值,而是要每次动态地从前面的接口响应中获取。 + +那要怎么确定该字段是在之前的哪个接口中返回的呢? + +操作方式也很简单,可以在抓包工具中对该字段进行搜索,特别地,搜索范围限定为响应内容(Response Header、Response Body)。 + +即可搜索得到该字段是在哪个接口中从服务器端返回值客户端的。 + +![](../images/charles-search-response.png) + +有时候可能搜索会得到多个结果,那么在确定是使用哪个接口响应的时候,遵循两个原则即可: + +- 响应一定是出现在当前接口之前 +- 如果在当前接口之前存在多个接口均有此返回,那么取最靠近当前接口的即可 + +通过前面的搜索可知,`X-CSRF-Token` 的值是在第一个接口中响应返回的。 + +![](../images/charles-locate-response-token.png) + +确定出具体的接口后,那么就可以在测试脚本中从该接口使用 `extract` 提取对应的字段,然后在后续接口中引用提取出的字段。 + +在当前案例中,第一个接口的响应内容为 HTML 页面,要提取字段可以使用正则匹配的方式。具体的做法就是指定目标字段的左右边界,目标字段使用 `(.*)` 匹配获取。 + +```yaml +- test: + name: /account/sign_in + request: + headers: + If-None-Match: W/"bc9ae267fdcbd89bf1dfaea10dea2b0e" + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + method: GET + url: https://testerhome.com/account/sign_in + extract: + X_CSRF_Token: <meta name="csrf-token" content="(.*)" /> + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] +``` + +然后,在后续使用到该字段的接口中,引用提取出的字段即可。 + +```yaml +- test: + name: /account/sign_in + request: + data: + commit: Sign In + user[login]: debugtalk + user[password]: XXXXXXXX + user[remember_me]: '1' + utf8: ✓ + headers: + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36 + X-CSRF-Token: $X_CSRF_Token + X-Requested-With: XMLHttpRequest + method: POST + url: https://testerhome.com/account/sign_in + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, text/html; charset=utf-8] +``` diff --git a/docs/images/HttpRunner-architecture-diagram.svg b/docs/images/HttpRunner-architecture-diagram.svg new file mode 100644 index 00000000..b6db8ac8 --- /dev/null +++ b/docs/images/HttpRunner-architecture-diagram.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="processonSvg1000" viewBox="92.33983462181887 27.4519054632815 540.5945487275026 2318.0242850129093" width="540.5945487275026" height="2318.0242850129093"><defs id="ProcessOnDefs1001"><marker id="ProcessOnMarker1241" markerUnits="userSpaceOnUse" orient="auto" markerWidth="16.23606797749979" markerHeight="10.550836550532098" viewBox="-1.0 -1.3763819204711736 16.23606797749979 10.550836550532098" refX="15.23606797749979" refY="3.8990363547948754"><path id="ProcessOnPath1242" d="M12.0 3.8990363547948754L0.0 7.798072709589751V0.0Z" stroke="#323232" stroke-width="2.0" fill="#323232" transform="matrix(-1.0,1.2246467991473532E-16,-1.2246467991473532E-16,-1.0,14.23606797749979,7.798072709589751)"/></marker><marker id="ProcessOnMarker1268" markerUnits="userSpaceOnUse" orient="auto" markerWidth="16.23606797749979" markerHeight="10.550836550532098" viewBox="-1.0 -1.3763819204711736 16.23606797749979 10.550836550532098" refX="-1.0" refY="3.8990363547948754"><path id="ProcessOnPath1269" d="M12.0 3.8990363547948754L0.0 7.798072709589751V0.0Z" stroke="#323232" stroke-width="2.0" fill="#323232" transform="matrix(1.0,0.0,0.0,1.0,0.0,0.0)"/></marker></defs><g id="ProcessOnG1002"><path id="ProcessOnPath1003" d="M92.33983462181887 27.4519054632815H632.9343833493215V2345.476190476191H92.33983462181891V27.45190546328149Z" fill="none"/><g id="ProcessOnG1004"><g id="ProcessOnG1005" transform="matrix(1.0,0.0,0.0,1.0,112.88542255465654,1086.611661066205)" opacity="1.0"><path id="ProcessOnPath1006" d="M0.0 0.0L498.48019689488 0.0L498.48019689488 139.7021897986965L0.0 139.7021897986965Z" stroke="#ff3333" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1007" transform="matrix(1.0,0.0,0.0,1.0,10.0,61.726094899348254)"><text id="ProcessOnText1008" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">initialize</text></g></g><g id="ProcessOnG1009" transform="matrix(1.0,0.0,0.0,1.0,112.61916208461707,1247.1053113553105)" opacity="1.0"><path id="ProcessOnPath1010" d="M0.0 0.0L500.3152212647043 0.0L500.3152212647043 346.7161172161177L0.0 346.7161172161177Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1011" transform="matrix(1.0,0.0,0.0,1.0,10.0,165.23305860805885)"><text id="ProcessOnText1012" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">add_tests</text></g></g><g id="ProcessOnG1013" transform="matrix(1.0,0.0,0.0,1.0,337.4134104565143,1267.0192416037708)" opacity="1.0"><path id="ProcessOnPath1014" d="M0.0 0.0L161.21581935060743 0.0L161.21581935060743 36.303075632226864L0.0 36.303075632226864Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccff99"/><g id="ProcessOnG1015" transform="matrix(1.0,0.0,0.0,1.0,10.0,10.026537816113432)"><text id="ProcessOnText1016" fill="#323232" font-size="13" x="69.60790967530372" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parsed testcase</text></g></g><g id="ProcessOnG1017" transform="matrix(1.0,0.0,0.0,1.0,282.9041074449991,1280.3754957963454)" opacity="1.0"><path id="ProcessOnPath1018" d="M0.0 0.0L161.21581935060743 0.0L161.21581935060743 36.303075632226864L0.0 36.303075632226864Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccff99"/><g id="ProcessOnG1019" transform="matrix(1.0,0.0,0.0,1.0,10.0,10.026537816113432)"><text id="ProcessOnText1020" fill="#323232" font-size="13" x="69.60790967530372" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parsed testcase</text></g></g><g id="ProcessOnG1021" transform="matrix(1.0,0.0,0.0,1.0,232.96367749151014,1444.6236263736241)" opacity="1.0"><path id="ProcessOnPath1022" d="M0.0 0.0L356.67372894043774 0.0L356.67372894043774 136.3406593406612L0.0 136.3406593406612Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1023" transform="matrix(1.0,0.0,0.0,1.0,10.0,58.1703296703306)"><text id="ProcessOnText1024" fill="#323232" font-size="16" x="0.0" y="16.4" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="start" size="16">unittest</text></g></g><g id="ProcessOnG1025" transform="matrix(1.0,0.0,0.0,1.0,327.12278250462106,1458.3890357311348)" opacity="1.0"><path id="ProcessOnPath1026" d="M0.0 0.0L250.93608328351058 0.0L250.93608328351058 113.28953569743635L0.0 113.28953569743635Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ff66"/><g id="ProcessOnG1027" transform="matrix(1.0,0.0,0.0,1.0,10.0,46.644767848718175)"><text id="ProcessOnText1028" fill="#323232" font-size="16" x="0.0" y="16.4" font-family="Times New Roman" font-weight="bold" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="start" size="16">TestSuite</text></g></g><g id="ProcessOnG1029" transform="matrix(1.0,0.0,0.0,1.0,113.38300174111309,220.83333333333334)" opacity="1.0"><path id="ProcessOnPath1030" d="M0.0 0.0L497.4850385219669 0.0L497.4850385219669 430.83333333333314L0.0 430.83333333333314Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1031" transform="matrix(1.0,0.0,0.0,1.0,10.0,207.29166666666657)"><text id="ProcessOnText1032" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">load_tests</text></g></g><g id="ProcessOnG1033" transform="matrix(1.0,0.0,0.0,1.0,233.11944069345282,521.6816654812568)" opacity="1.0"><path id="ProcessOnPath1034" d="M0.0 0.0L367.08430389944095 0.0L367.08430389944095 65.81833451874252L0.0 65.81833451874252Z" stroke="#3399ff" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1035" transform="matrix(1.0,0.0,0.0,1.0,10.0,0.0)"><text id="ProcessOnText1036" fill="#00cc66" font-size="13" x="172.54215194972048" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">validate</text></g></g><g id="ProcessOnG1037" transform="matrix(1.0,0.0,0.0,1.0,112.33983462181887,670.4423076923075)" opacity="1.0"><path id="ProcessOnPath1038" d="M0.0 0.0L499.0132324826069 0.0L499.0132324826069 397.93269230769215L0.0 397.93269230769215Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1039" transform="matrix(1.0,0.0,0.0,1.0,10.0,190.84134615384608)"><text id="ProcessOnText1040" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">parse_tests</text></g></g><g id="ProcessOnG1041" transform="matrix(1.0,0.0,0.0,1.0,113.21047703177533,2207.648206200245)" opacity="1.0"><path id="ProcessOnPath1042" d="M0.0 0.0L499.1325913703879 0.0L499.1325913703879 117.82798427594548L0.0 117.82798427594548Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1043" transform="matrix(1.0,0.0,0.0,1.0,10.0,50.78899213797274)"><text id="ProcessOnText1044" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">report</text></g></g><g id="ProcessOnG1045" transform="matrix(1.0,0.0,0.0,1.0,112.66717341046166,1609.1814672255873)" opacity="1.0"><path id="ProcessOnPath1046" d="M0.0 0.0L500.2191986130151 0.0L500.2191986130151 475.6172242789887L0.0 475.6172242789887Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1047" transform="matrix(1.0,0.0,0.0,1.0,10.0,229.68361213949436)"><text id="ProcessOnText1048" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">runner</text></g></g><g id="ProcessOnG1049" transform="matrix(1.0,0.0,0.0,1.0,232.5248560973461,1678.309523809528)" opacity="1.0"><path id="ProcessOnPath1050" d="M0.0 0.0L360.3774785009274 0.0L360.3774785009274 337.36111111111074L0.0 337.36111111111074Z" stroke="#66cc00" stroke-width="1.0" stroke-dasharray="1.0 1.5" fill="#cce5ff"/><g id="ProcessOnG1051" transform="matrix(1.0,0.0,0.0,1.0,10.0,152.43055555555537)"><text id="ProcessOnText1052" fill="#ff00ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">teststep</text><text id="ProcessOnText1053" fill="#ff00ff" font-size="13" x="0.0" y="29.575" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">iteration</text></g></g><g id="ProcessOnG1054" transform="matrix(1.0,0.0,0.0,1.0,113.27041379948983,47.4519054632815)" opacity="1.0"><path id="ProcessOnPath1055" d="M0.0 0.0L498.3614661200861 0.0L498.3614661200861 158.0152909970355L0.0 158.0152909970355Z" stroke="#ff3333" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1056" transform="matrix(1.0,0.0,0.0,1.0,10.0,70.88264549851775)"><text id="ProcessOnText1057" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13"> prepare</text></g></g><g id="ProcessOnG1058" transform="matrix(1.0,0.0,0.0,1.0,234.06815237230134,67.42662086911432)" opacity="1.0"><path id="ProcessOnPath1059" d="M0.0 0.0L119.9535525822713 0.0L119.9535525822713 38.696035837626695L0.0 38.696035837626695Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccccff"/><g id="ProcessOnG1060" transform="matrix(1.0,0.0,0.0,1.0,10.0,4.348017918813348)"><text id="ProcessOnText1061" fill="#323232" font-size="12" x="48.97677629113565" y="12.3" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="12">HAR</text><text id="ProcessOnText1062" fill="#323232" font-size="12" x="48.97677629113565" y="27.3" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="12">Charles/Fiddler</text></g></g><g id="ProcessOnG1063" transform="matrix(1.0,0.0,0.0,1.0,354.0217049545728,67.28919367977625)" opacity="1.0"><path id="ProcessOnPath1064" d="M0.0 0.0L120.57366391374057 0.0L120.57366391374057 39.56268990143752L0.0 39.56268990143752Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccffff"/><g id="ProcessOnG1065" transform="matrix(1.0,0.0,0.0,1.0,10.0,3.5313449507187613)"><text id="ProcessOnText1066" fill="#323232" font-size="13" x="49.28683195687029" y="13.325" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">Postman</text><text id="ProcessOnText1067" fill="#323232" font-size="13" x="49.28683195687029" y="29.575" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">collection</text></g></g><g id="ProcessOnG1068" transform="matrix(1.0,0.0,0.0,1.0,234.06815237230134,106.12265670674105)" opacity="1.0"><path id="ProcessOnPath1069" d="M0.0 0.0L240.32661118015523 0.0L240.32661118015523 40.59549987498171L0.0 40.59549987498171Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffff66"/><g id="ProcessOnG1070" transform="matrix(1.0,0.0,0.0,1.0,10.0,12.172749937490856)"><text id="ProcessOnText1071" fill="#323232" font-size="13" x="109.16330559007761" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">record & generate</text></g></g><g id="ProcessOnG1072" transform="matrix(1.0,0.0,0.0,1.0,234.06815237230134,146.71815658172244)" opacity="1.0"><path id="ProcessOnPath1073" d="M0.0 0.0L119.9535525822713 0.0L119.9535525822713 39.673719164991724L0.0 39.673719164991724Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#cfcfcf"/><g id="ProcessOnG1074" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.711859582495862)"><text id="ProcessOnText1075" fill="#323232" font-size="13" x="48.97677629113565" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">JSON</text></g></g><g id="ProcessOnG1076" transform="matrix(1.0,0.0,0.0,1.0,233.4234599167679,458.89988419640457)" opacity="1.0"><path id="ProcessOnPath1077" d="M0.0 0.0L366.4762654528107 0.0L366.4762654528107 45.76137121485101L0.0 45.76137121485101Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1078" transform="matrix(1.0,0.0,0.0,1.0,10.0,14.755685607425505)"><text id="ProcessOnText1079" fill="#323232" font-size="13" x="0.0" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="start" size="13">file loader</text></g></g><g id="ProcessOnG1080" transform="matrix(1.0,0.0,0.0,1.0,349.94281175168095,467.6351780925447)" opacity="1.0"><path id="ProcessOnPath1081" d="M0.0 0.0L60.11083217216599 0.0L60.11083217216599 26.72838770448834L0.0 26.72838770448834Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1082" transform="matrix(1.0,0.0,0.0,1.0,10.0,5.239193852244171)"><text id="ProcessOnText1083" fill="#323232" font-size="13" x="19.055416086082996" y="13.325" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">api</text></g></g><g id="ProcessOnG1084" transform="matrix(1.0,0.0,0.0,1.0,420.2674902757755,468.199250711168)" opacity="1.0"><path id="ProcessOnPath1085" d="M0.0 0.0L71.26713035346097 0.0L71.26713035346097 26.0709230991921L0.0 26.0709230991921Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1086" transform="matrix(1.0,0.0,0.0,1.0,10.0,-3.21453845040395)"><text id="ProcessOnText1087" fill="#323232" font-size="13" x="24.633565176730485" y="13.325" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">testcase</text><text id="ProcessOnText1088" fill="#323232" font-size="13" x="24.633565176730485" y="29.575" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">s</text></g></g><g id="ProcessOnG1089" transform="matrix(1.0,0.0,0.0,1.0,503.84647887455526,469.15661248130175)" opacity="1.0"><path id="ProcessOnPath1090" d="M0.0 0.0L68.14058888428156 0.0L68.14058888428156 25.247914645056653L0.0 25.247914645056653Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1091" transform="matrix(1.0,0.0,0.0,1.0,10.0,-3.6260426774716734)"><text id="ProcessOnText1092" fill="#323232" font-size="13" x="23.070294442140778" y="13.325" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">testsuite</text><text id="ProcessOnText1093" fill="#323232" font-size="13" x="23.070294442140778" y="29.575" font-family="Times New Roman" font-weight="normal" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="13">s</text></g></g><g id="ProcessOnG1094" transform="matrix(1.0,0.0,0.0,1.0,228.91043277822644,958.5410658480989)" opacity="1.0"><path id="ProcessOnPath1095" d="M0.0 0.0L157.96737186622707 0.0L157.96737186622707 38.636618204456L0.0 38.636618204456Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1096" transform="matrix(1.0,0.0,0.0,1.0,10.0,3.0683091022279996)"><text id="ProcessOnText1097" fill="#323232" font-size="13" x="67.98368593311353" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parse config </text><text id="ProcessOnText1098" fill="#323232" font-size="13" x="67.98368593311353" y="29.575" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">name</text></g></g><g id="ProcessOnG1099" transform="matrix(1.0,0.0,0.0,1.0,229.42258084981347,1013.1071428571423)" opacity="1.0"><path id="ProcessOnPath1100" d="M0.0 0.0L360.26462701927693 0.0L360.26462701927693 38.571428571428555L0.0 38.571428571428555Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccff99"/><g id="ProcessOnG1101" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.160714285714278)"><text id="ProcessOnText1102" fill="#323232" font-size="13" x="169.13231350963846" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parsed testcases(list)</text></g></g><g id="ProcessOnG1103" transform="matrix(1.0,0.0,0.0,1.0,408.2012022233929,959.0295723678073)" opacity="1.0"><path id="ProcessOnPath1104" d="M0.0 0.0L180.5538936805524 0.0L180.5538936805524 37.65960516503924L0.0 37.65960516503924Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1105" transform="matrix(1.0,0.0,0.0,1.0,10.0,2.579802582519619)"><text id="ProcessOnText1106" fill="#323232" font-size="13" x="79.2769468402762" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parse config </text><text id="ProcessOnText1107" fill="#323232" font-size="13" x="79.2769468402762" y="29.575" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">request</text></g></g><g id="ProcessOnG1108" transform="matrix(1.0,0.0,0.0,1.0,242.07165402404743,538.5198465225878)" opacity="1.0"><path id="ProcessOnPath1109" d="M0.0 0.0L168.43722155141722 0.0L168.43722155141722 38.75753022080437L0.0 38.75753022080437Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1110" transform="matrix(1.0,0.0,0.0,1.0,10.0,3.1287651104021847)"><text id="ProcessOnText1111" fill="#323232" font-size="13" x="73.21861077570861" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">JSON Schema </text><text id="ProcessOnText1112" fill="#323232" font-size="13" x="73.21861077570861" y="29.575" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">Validate</text></g></g><g id="ProcessOnG1113" transform="matrix(1.0,0.0,0.0,1.0,232.30895751688905,1359.4196425746597)" opacity="1.0"><path id="ProcessOnPath1114" d="M0.0 0.0L293.0503870024336 0.0L293.0503870024336 57.78913651707876L0.0 57.78913651707876Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1115" transform="matrix(1.0,0.0,0.0,1.0,10.0,18.89456825853938)"><text id="ProcessOnText1116" fill="#323232" font-size="16" x="0.0" y="16.4" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="start" size="16">Runner()</text></g></g><g id="ProcessOnG1117" transform="matrix(1.0,0.0,0.0,1.0,325.9341795765353,1368.814210833199)" opacity="1.0"><path id="ProcessOnPath1118" d="M0.0 0.0L77.33840442160249 0.0L77.33840442160249 39.0L0.0 39.0Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1119" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.375)"><text id="ProcessOnText1120" fill="#323232" font-size="13" x="27.669202210801245" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">Context()</text></g></g><g id="ProcessOnG1121" transform="matrix(1.0,0.0,0.0,1.0,411.2245609624021,1368.8142108331995)" opacity="1.0"><path id="ProcessOnPath1122" d="M0.0 0.0L98.79213668284427 0.0L98.79213668284427 38.99999999999909L0.0 38.99999999999909Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1123" transform="matrix(1.0,0.0,0.0,1.0,10.0,3.2499999999995453)"><text id="ProcessOnText1124" fill="#323232" font-size="13" x="38.396068341422136" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">HttpSession(</text><text id="ProcessOnText1125" fill="#323232" font-size="13" x="38.396068341422136" y="29.575" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">)</text></g></g><g id="ProcessOnG1126" transform="matrix(1.0,0.0,0.0,1.0,408.2864240537298,1474.2390109890125)" opacity="1.0"><path id="ProcessOnPath1127" d="M0.0 0.0L154.08327717118993 0.0L154.08327717118993 35.29670329670148L0.0 35.29670329670148Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ff66"/><g id="ProcessOnG1128" transform="matrix(1.0,0.0,0.0,1.0,10.0,8.89835164835074)"><text id="ProcessOnText1129" fill="#323232" font-size="14" x="66.04163858559497" y="14.350000000000001" font-family="Times New Roman" font-weight="bold" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="14">TestCase</text></g></g><g id="ProcessOnG1130" transform="matrix(1.0,0.0,0.0,1.0,232.39060265065177,1627.630005948649)" opacity="1.0"><path id="ProcessOnPath1131" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ffff"/><g id="ProcessOnG1132" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1133" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">testcase setup hooks</text></g></g><g id="ProcessOnG1134" transform="matrix(1.0,0.0,0.0,1.0,232.24361590786273,2030.5978422059552)" opacity="1.0"><path id="ProcessOnPath1135" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#33ffff"/><g id="ProcessOnG1136" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1137" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">testcase teardown hooks</text></g></g><g id="ProcessOnG1138" transform="matrix(1.0,0.0,0.0,1.0,332.1762113922986,1689.4957523369387)" opacity="1.0"><path id="ProcessOnPath1139" d="M0.0 0.0L240.82922550815533 0.0L240.82922550815533 37.89473684210532L0.0 37.89473684210532Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#cfcfcf"/><g id="ProcessOnG1140" transform="matrix(1.0,0.0,0.0,1.0,10.0,10.822368421052659)"><text id="ProcessOnText1141" fill="#323232" font-size="13" x="109.41461275407767" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">check if skip</text></g></g><g id="ProcessOnG1142" transform="matrix(1.0,0.0,0.0,1.0,332.17621139229874,1727.3904891790442)" opacity="1.0"><path id="ProcessOnPath1143" d="M0.0 0.0L240.82922550815522 0.0L240.82922550815522 39.18546365914767L0.0 39.18546365914767Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffcce6"/><g id="ProcessOnG1144" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.467731829573836)"><text id="ProcessOnText1145" fill="#323232" font-size="13" x="109.41461275407761" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">prepare API</text></g></g><g id="ProcessOnG1146" transform="matrix(1.0,0.0,0.0,1.0,331.95422786204443,1766.5759528381918)" opacity="1.0"><path id="ProcessOnPath1147" d="M0.0 0.0L241.27319256866372 0.0L241.27319256866372 40.476190476190595L0.0 40.476190476190595Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ffff"/><g id="ProcessOnG1148" transform="matrix(1.0,0.0,0.0,1.0,10.0,12.113095238095298)"><text id="ProcessOnText1149" fill="#323232" font-size="13" x="109.63659628433186" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">api setup hooks</text></g></g><g id="ProcessOnG1150" transform="matrix(1.0,0.0,0.0,1.0,331.791643635635,1807.0521433143822)" opacity="1.0"><path id="ProcessOnPath1151" d="M0.0 0.0L241.59836102148256 0.0L241.59836102148256 39.63659147869646L0.0 39.63659147869646Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffff00"/><g id="ProcessOnG1152" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.69329573934823)"><text id="ProcessOnText1153" fill="#323232" font-size="13" x="109.79918051074128" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">send request</text></g></g><g id="ProcessOnG1154" transform="matrix(1.0,0.0,0.0,1.0,331.95422786204466,1885.34137966283)" opacity="1.0"><path id="ProcessOnPath1155" d="M0.0 0.0L241.2731925686635 0.0L241.2731925686635 39.6806884635829L0.0 39.6806884635829Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ffff"/><g id="ProcessOnG1156" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.715344231791448)"><text id="ProcessOnText1157" fill="#323232" font-size="13" x="109.63659628433174" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">api teardown hooks</text></g></g><g id="ProcessOnG1158" transform="matrix(1.0,0.0,0.0,1.0,331.95422786204466,1925.3413796628301)" opacity="1.0"><path id="ProcessOnPath1159" d="M0.0 0.0L241.2731925686635 0.0L241.2731925686635 39.6806884635829L0.0 39.6806884635829Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffb366"/><g id="ProcessOnG1160" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.715344231791448)"><text id="ProcessOnText1161" fill="#323232" font-size="13" x="109.63659628433174" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">extract response</text></g></g><g id="ProcessOnG1162" transform="matrix(1.0,0.0,0.0,1.0,332.4427102572061,1965.022068126409)" opacity="1.0"><path id="ProcessOnPath1163" d="M0.0 0.0L240.2962277783406 0.0L240.2962277783406 40.416666666666515L0.0 40.416666666666515Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ff66ff"/><g id="ProcessOnG1164" transform="matrix(1.0,0.0,0.0,1.0,10.0,12.083333333333258)"><text id="ProcessOnText1165" fill="#323232" font-size="13" x="109.1481138891703" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">validate</text></g></g><g id="ProcessOnG1166" transform="matrix(1.0,0.0,0.0,1.0,112.9448745740657,2107.4739777602435)" opacity="1.0"><path id="ProcessOnPath1167" d="M0.0 0.0L499.66379628580717 0.0L499.66379628580717 77.823641287373L0.0 77.823641287373Z" stroke="#ff0000" stroke-width="1.0" stroke-dasharray="5.0 2.0" fill="none"/><g id="ProcessOnG1168" transform="matrix(1.0,0.0,0.0,1.0,10.0,30.786820643686497)"><text id="ProcessOnText1169" fill="#3333ff" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="bold" font-style="normal" text-decoration="none" family="Arial" text-anchor="start" size="13">aggregate</text></g></g><g id="ProcessOnG1170" transform="matrix(1.0,0.0,0.0,1.0,232.75387687043911,2128.518477126587)" opacity="1.0"><path id="ProcessOnPath1171" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffff66"/><g id="ProcessOnG1172" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1173" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">aggregate results</text></g></g><g id="ProcessOnG1174" transform="matrix(1.0,0.0,0.0,1.0,232.30362025420487,2268.585331342556)" opacity="1.0"><path id="ProcessOnPath1175" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffff66"/><g id="ProcessOnG1176" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1177" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">generate html report</text></g></g><g id="ProcessOnG1178" transform="matrix(1.0,0.0,0.0,1.0,232.75387687043911,2227.797619047623)" opacity="1.0"><path id="ProcessOnPath1179" d="M0.0 0.0L180.45231324637427 0.0L180.45231324637427 40.592448934823096L0.0 40.592448934823096Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#99ccff"/><g id="ProcessOnG1180" transform="matrix(1.0,0.0,0.0,1.0,10.0,12.171224467411548)"><text id="ProcessOnText1181" fill="#323232" font-size="13" x="79.22615662318714" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">default template</text></g></g><g id="ProcessOnG1182" transform="matrix(1.0,0.0,0.0,1.0,413.2061901168134,2227.388673713562)" opacity="1.0"><path id="ProcessOnPath1183" d="M0.0 0.0L179.69614448146007 0.0L179.69614448146007 41.41033960294408L0.0 41.41033960294408Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#9999ff"/><g id="ProcessOnG1184" transform="matrix(1.0,0.0,0.0,1.0,10.0,12.580169801472039)"><text id="ProcessOnText1185" fill="#323232" font-size="13" x="78.84807224073003" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">custom template</text></g></g><g id="ProcessOnG1186" transform="matrix(1.0,0.0,0.0,1.0,331.49653118821806,1846.8084766192353)" opacity="1.0"><path id="ProcessOnPath1187" d="M0.0 0.0L242.18858591631658 0.0L242.18858591631658 38.58438052362533L0.0 38.58438052362533Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#99ccff"/><g id="ProcessOnG1188" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.167190261812664)"><text id="ProcessOnText1189" fill="#323232" font-size="13" x="110.09429295815829" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">make ResponseObject</text></g></g><g id="ProcessOnG1190" transform="matrix(1.0,0.0,0.0,1.0,354.44121097018535,146.7181565817225)" opacity="1.0"><path id="ProcessOnPath1191" d="M0.0 0.0L119.9535525822713 0.0L119.9535525822713 39.673719164991724L0.0 39.673719164991724Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#cfcfcf"/><g id="ProcessOnG1192" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.711859582495862)"><text id="ProcessOnText1193" fill="#323232" font-size="13" x="48.97677629113565" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">YAML</text></g></g><g id="ProcessOnG1194" transform="matrix(1.0,0.0,0.0,1.0,489.0512741938859,146.83624959819713)" opacity="1.0"><path id="ProcessOnPath1195" d="M0.0 0.0L110.17512558531087 0.0L110.17512558531087 39.43753313204235L0.0 39.43753313204235Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#cfcfcf"/><g id="ProcessOnG1196" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.593766566021174)"><text id="ProcessOnText1197" fill="#323232" font-size="13" x="44.08756279265543" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">.env</text></g></g><g id="ProcessOnG1198" transform="matrix(1.0,0.0,0.0,1.0,488.6776347452603,86.44114827147351)" opacity="1.0"><path id="ProcessOnPath1199" d="M0.0 0.0L110.92240448256211 0.0L110.92240448256211 39.67789934757417L0.0 39.67789934757417Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#b8b8b8"/><g id="ProcessOnG1200" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.713949673787084)"><text id="ProcessOnText1201" fill="#323232" font-size="13" x="44.46120224128106" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">debugtalk.py</text></g></g><g id="ProcessOnG1202" transform="matrix(1.0,0.0,0.0,1.0,233.46824659817423,599.7531256992314)" opacity="1.0"><path id="ProcessOnPath1203" d="M0.0 0.0L366.3866920899981 0.0L366.3866920899981 39.41354096743453L0.0 39.41354096743453Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffcc99"/><g id="ProcessOnG1204" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.581770483717264)"><text id="ProcessOnText1205" fill="#323232" font-size="13" x="172.19334604499906" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">Raw valid data</text></g></g><g id="ProcessOnG1206" transform="matrix(1.0,0.0,0.0,1.0,429.3905887134148,538.4399184115022)" opacity="1.0"><path id="ProcessOnPath1207" d="M0.0 0.0L153.94780990051652 0.0L153.94780990051652 38.91738644297561L0.0 38.91738644297561Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1208" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.333693221487806)"><text id="ProcessOnText1209" fill="#323232" font-size="13" x="65.97390495025826" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">.env validate</text></g></g><g id="ProcessOnG1210" transform="matrix(1.0,0.0,0.0,1.0,233.64450265680318,347.6646610093986)" opacity="1.0"><path id="ProcessOnPath1211" d="M0.0 0.0L366.11895624108945 0.0L366.11895624108945 42.97375366848877L0.0 42.97375366848877Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1212" transform="matrix(1.0,0.0,0.0,1.0,10.0,13.361876834244384)"><text id="ProcessOnText1213" fill="#323232" font-size="13" x="172.05947812054472" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">import builtin module</text></g></g><g id="ProcessOnG1214" transform="matrix(1.0,0.0,0.0,1.0,229.5184860637555,852.6089343710092)" opacity="1.0"><path id="ProcessOnPath1215" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1216" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1217" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parse config variables</text></g></g><g id="ProcessOnG1218" transform="matrix(1.0,0.0,0.0,1.0,229.0497360637555,904.4839343710094)" opacity="1.0"><path id="ProcessOnPath1219" d="M0.0 0.0L360.5987143440685 0.0L360.5987143440685 39.836786031304655L0.0 39.836786031304655Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1220" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.793393015652327)"><text id="ProcessOnText1221" fill="#323232" font-size="13" x="169.29935717203426" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parse config parameters</text></g></g><g id="ProcessOnG1222" transform="matrix(1.0,0.0,0.0,1.0,230.20229693857172,690.9282891832243)" opacity="1.0"><path id="ProcessOnPath1223" d="M0.0 0.0L143.64506571884846 0.0L143.64506571884846 92.18068517574778L0.0 92.18068517574778Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1224" transform="matrix(1.0,0.0,0.0,1.0,10.0,37.96534258787389)"><text id="ProcessOnText1225" fill="#323232" font-size="13" x="60.82253285942423" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">extend & merge </text></g></g><g id="ProcessOnG1226" transform="matrix(1.0,0.0,0.0,1.0,230.20614864773185,796.4279508740539)" opacity="1.0"><path id="ProcessOnPath1227" d="M0.0 0.0L360.07936348760376 0.0L360.07936348760376 39.097690151583606L0.0 39.097690151583606Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccccff"/><g id="ProcessOnG1228" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.423845075791803)"><text id="ProcessOnText1229" fill="#323232" font-size="13" x="169.03968174380188" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">valid testcases</text></g></g><g id="ProcessOnG1230" transform="matrix(1.0,0.0,0.0,1.0,373.739891080212,690.9282891832243)" opacity="1.0"><path id="ProcessOnPath1231" d="M0.0 0.0L215.01520482373337 0.0L215.01520482373337 46.24871870210825L0.0 46.24871870210825Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1232" transform="matrix(1.0,0.0,0.0,1.0,10.0,6.8743593510541245)"><text id="ProcessOnText1233" fill="#323232" font-size="13" x="96.50760241186669" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">api: validate, extract, </text><text id="ProcessOnText1234" fill="#323232" font-size="13" x="96.50760241186669" y="29.575" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">variables</text></g></g><g id="ProcessOnG1235" transform="matrix(1.0,0.0,0.0,1.0,373.84736265742,737.1770078853325)" opacity="1.0"><path id="ProcessOnPath1236" d="M0.0 0.0L214.80026166931736 0.0L214.80026166931736 45.93196647363948L0.0 45.93196647363948Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1237" transform="matrix(1.0,0.0,0.0,1.0,10.0,14.840983236819739)"><text id="ProcessOnText1238" fill="#323232" font-size="13" x="0.0" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="start" size="13">testcase: variables</text></g></g><g id="ProcessOnG1239"><path id="ProcessOnPath1240" d="M485.32806263932474 1459.0029430115128L485.32806263932474 1432.8667521832322L378.8341510181058 1432.8667521832322L378.8341510181058 1417.2087790917383" stroke="#323232" stroke-width="2.0" stroke-dasharray="none" fill="none" marker-start="url(#ProcessOnMarker1241)"/><g id="ProcessOnG1243" transform="matrix(1.0,0.0,0.0,1.0,437.32021569710867,1424.7417521832322)"><path id="ProcessOnPath1244" d="M0 0H21.0V16.25H0Z" fill="#fff" transform="matrix(1.0,0.0,0.0,1.0,-10.5,0.0)"/><text id="ProcessOnText1245" fill="#323232" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">add</text></g></g><g id="ProcessOnG1246" transform="matrix(1.0,0.0,0.0,1.0,408.37854469650955,1524.927937447169)" opacity="1.0"><path id="ProcessOnPath1247" d="M0.0 0.0L154.08327717118993 0.0L154.08327717118993 35.3220625528304L0.0 35.3220625528304Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ff66"/><g id="ProcessOnG1248" transform="matrix(1.0,0.0,0.0,1.0,10.0,8.911031276415201)"><text id="ProcessOnText1249" fill="#323232" font-size="14" x="66.04163858559497" y="14.350000000000001" font-family="Times New Roman" font-weight="bold" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="14">TestCase</text></g></g><g id="ProcessOnG1250" transform="matrix(1.0,0.0,0.0,1.0,233.98302374780596,1098.028136579187)" opacity="1.0"><path id="ProcessOnPath1251" d="M0.0 0.0L365.9895999927588 0.0L365.9895999927588 116.86923877273216L0.0 116.86923877273216Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1252" transform="matrix(1.0,0.0,0.0,1.0,10.0,48.43461938636608)"><text id="ProcessOnText1253" fill="#323232" font-size="16" x="0.0" y="16.4" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="start" size="16">unittest</text></g></g><g id="ProcessOnG1254" transform="matrix(1.0,0.0,0.0,1.0,334.1177981996935,1107.5082529067683)" opacity="1.0"><path id="ProcessOnPath1255" d="M0.0 0.0L251.563161963354 0.0L251.563161963354 43.72245577848298L0.0 43.72245577848298Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ff66"/><g id="ProcessOnG1256" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.86122788924149)"><text id="ProcessOnText1257" fill="#323232" font-size="16" x="114.781580981677" y="16.4" font-family="Times New Roman" font-weight="bold" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="16">TextTestRunner()</text></g></g><g id="ProcessOnG1258" transform="matrix(1.0,0.0,0.0,1.0,334.1177981996935,1159.9368243353392)" opacity="1.0"><path id="ProcessOnPath1259" d="M0.0 0.0L251.563161963354 0.0L251.563161963354 43.72245577848298L0.0 43.72245577848298Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#66ff66"/><g id="ProcessOnG1260" transform="matrix(1.0,0.0,0.0,1.0,10.0,11.86122788924149)"><text id="ProcessOnText1261" fill="#323232" font-size="16" x="114.781580981677" y="16.4" font-family="Times New Roman" font-weight="bold" font-style="normal" text-decoration="none" family="Times New Roman" text-anchor="middle" size="16">TestLoader()</text></g></g><g id="ProcessOnG1262" transform="matrix(1.0,0.0,0.0,1.0,232.3079358053183,1293.8278767487263)" opacity="1.0"><path id="ProcessOnPath1263" d="M0.0 0.0L158.33606527198432 0.0L158.33606527198432 34.27926610841632L0.0 34.27926610841632Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ccff99"/><g id="ProcessOnG1264" transform="matrix(1.0,0.0,0.0,1.0,10.0,9.014633054208161)"><text id="ProcessOnText1265" fill="#323232" font-size="13" x="68.16803263599216" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">parsed testcase</text></g></g><g id="ProcessOnG1266"><path id="ProcessOnPath1267" d="M311.47596844131044 1328.1071428571427L311.47596844131044 1336.620535573044L378.8341510181059 1336.620535573044L378.8341510181059 1344.18357459716" stroke="#323232" stroke-width="2.0" stroke-dasharray="none" fill="none" marker-end="url(#ProcessOnMarker1268)"/><g id="ProcessOnG1270" transform="matrix(1.0,0.0,0.0,1.0,344.67988288381554,1328.495535573044)"><path id="ProcessOnPath1271" d="M0 0H17.0V16.25H0Z" fill="#fff" transform="matrix(1.0,0.0,0.0,1.0,-8.5,0.0)"/><text id="ProcessOnText1272" fill="#323232" font-size="13" x="0.0" y="13.325" font-family="Arial" font-weight="normal" font-style="normal" text-decoration="none" family="Arial" text-anchor="middle" size="13">init</text></g></g><g id="ProcessOnG1273" transform="matrix(1.0,0.0,0.0,1.0,234.43565446172528,235.944370692969)" opacity="1.0"><path id="ProcessOnPath1274" d="M0.0 0.0L366.12630441645973 0.0L366.12630441645973 48.8283565797575L0.0 48.8283565797575Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ff99ff"/><g id="ProcessOnG1275" transform="matrix(1.0,0.0,0.0,1.0,10.0,16.28917828987875)"><text id="ProcessOnText1276" fill="#323232" font-size="13" x="172.06315220822987" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">locate debugtalk.py => PWD</text></g></g><g id="ProcessOnG1277" transform="matrix(1.0,0.0,0.0,1.0,233.95448604496886,294.33132767606537)" opacity="1.0"><path id="ProcessOnPath1278" d="M0.0 0.0L366.11895624108945 0.0L366.11895624108945 42.97375366848877L0.0 42.97375366848877Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1279" transform="matrix(1.0,0.0,0.0,1.0,10.0,13.361876834244384)"><text id="ProcessOnText1280" fill="#323232" font-size="13" x="172.05947812054472" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">load custom.env / .env</text></g></g><g id="ProcessOnG1281" transform="matrix(1.0,0.0,0.0,1.0,233.60211452262854,401.8313276760655)" opacity="1.0"><path id="ProcessOnPath1282" d="M0.0 0.0L366.11895624108945 0.0L366.11895624108945 42.97375366848877L0.0 42.97375366848877Z" stroke="#323232" stroke-width="1.0" stroke-dasharray="none" fill="#ffffff"/><g id="ProcessOnG1283" transform="matrix(1.0,0.0,0.0,1.0,10.0,13.361876834244384)"><text id="ProcessOnText1284" fill="#323232" font-size="13" x="172.05947812054472" y="13.325" font-family="Courier New" font-weight="bold" font-style="normal" text-decoration="none" family="Courier New" text-anchor="middle" size="13">import debugtalk.py module</text></g></g></g></g></svg> \ No newline at end of file diff --git a/docs/images/charles-export-har.png b/docs/images/charles-export-har.png new file mode 100644 index 00000000..82db6a05 Binary files /dev/null and b/docs/images/charles-export-har.png differ diff --git a/docs/images/charles-export.jpg b/docs/images/charles-export.jpg new file mode 100644 index 00000000..a6d2ef7d Binary files /dev/null and b/docs/images/charles-export.jpg differ diff --git a/docs/images/charles-locate-response-token.png b/docs/images/charles-locate-response-token.png new file mode 100644 index 00000000..edab6fa2 Binary files /dev/null and b/docs/images/charles-locate-response-token.png differ diff --git a/docs/images/charles-save-har.jpg b/docs/images/charles-save-har.jpg new file mode 100644 index 00000000..1c86ed3c Binary files /dev/null and b/docs/images/charles-save-har.jpg differ diff --git a/docs/images/charles-search-response.png b/docs/images/charles-search-response.png new file mode 100644 index 00000000..88c7e04a Binary files /dev/null and b/docs/images/charles-search-response.png differ diff --git a/docs/images/demo-quickstart-http-1.jpg b/docs/images/demo-quickstart-http-1.jpg new file mode 100644 index 00000000..ae89002e Binary files /dev/null and b/docs/images/demo-quickstart-http-1.jpg differ diff --git a/docs/images/demo-quickstart-http-2.jpg b/docs/images/demo-quickstart-http-2.jpg new file mode 100644 index 00000000..d831f096 Binary files /dev/null and b/docs/images/demo-quickstart-http-2.jpg differ diff --git a/docs/images/loadtest-schematic-diagram.jpg b/docs/images/loadtest-schematic-diagram.jpg new file mode 100644 index 00000000..1d774c61 Binary files /dev/null and b/docs/images/loadtest-schematic-diagram.jpg differ diff --git a/docs/images/locusts-full-speed.jpg b/docs/images/locusts-full-speed.jpg new file mode 100644 index 00000000..67efa3ea Binary files /dev/null and b/docs/images/locusts-full-speed.jpg differ diff --git a/docs/images/qrcode_for_httprunner.jpg b/docs/images/qrcode_for_httprunner.jpg new file mode 100644 index 00000000..80687bbf Binary files /dev/null and b/docs/images/qrcode_for_httprunner.jpg differ diff --git a/docs/images/report-demo-quickstart-1-log1.jpg b/docs/images/report-demo-quickstart-1-log1.jpg new file mode 100644 index 00000000..c390732a Binary files /dev/null and b/docs/images/report-demo-quickstart-1-log1.jpg differ diff --git a/docs/images/report-demo-quickstart-1-log2.jpg b/docs/images/report-demo-quickstart-1-log2.jpg new file mode 100644 index 00000000..6eaddeae Binary files /dev/null and b/docs/images/report-demo-quickstart-1-log2.jpg differ diff --git a/docs/images/report-demo-quickstart-1-overview.jpg b/docs/images/report-demo-quickstart-1-overview.jpg new file mode 100644 index 00000000..27a85fa5 Binary files /dev/null and b/docs/images/report-demo-quickstart-1-overview.jpg differ diff --git a/docs/images/report-demo-quickstart-1-traceback.jpg b/docs/images/report-demo-quickstart-1-traceback.jpg new file mode 100644 index 00000000..a830102f Binary files /dev/null and b/docs/images/report-demo-quickstart-1-traceback.jpg differ diff --git a/docs/images/run-demo-quickstart-0.jpg b/docs/images/run-demo-quickstart-0.jpg new file mode 100644 index 00000000..287634fc Binary files /dev/null and b/docs/images/run-demo-quickstart-0.jpg differ diff --git a/docs/images/run-demo-quickstart-1.jpg b/docs/images/run-demo-quickstart-1.jpg new file mode 100644 index 00000000..34f72a60 Binary files /dev/null and b/docs/images/run-demo-quickstart-1.jpg differ diff --git a/docs/images/run-demo-quickstart-2.jpg b/docs/images/run-demo-quickstart-2.jpg new file mode 100644 index 00000000..55fe4bc8 Binary files /dev/null and b/docs/images/run-demo-quickstart-2.jpg differ diff --git a/docs/images/run-demo-quickstart-6.jpg b/docs/images/run-demo-quickstart-6.jpg new file mode 100644 index 00000000..40e69055 Binary files /dev/null and b/docs/images/run-demo-quickstart-6.jpg differ diff --git a/docs/images/testcase-layer.png b/docs/images/testcase-layer.png new file mode 100644 index 00000000..bcb8b6f3 Binary files /dev/null and b/docs/images/testcase-layer.png differ diff --git a/docs/images/testcase-structure.png b/docs/images/testcase-structure.png new file mode 100644 index 00000000..f63c0024 Binary files /dev/null and b/docs/images/testcase-structure.png differ diff --git a/docs/images/testerhome-charles-export.png b/docs/images/testerhome-charles-export.png new file mode 100644 index 00000000..2a981acf Binary files /dev/null and b/docs/images/testerhome-charles-export.png differ diff --git a/docs/images/testerhome-login-charles.png b/docs/images/testerhome-login-charles.png new file mode 100644 index 00000000..cbaf7d37 Binary files /dev/null and b/docs/images/testerhome-login-charles.png differ diff --git a/docs/images/testerhome-login.png b/docs/images/testerhome-login.png new file mode 100644 index 00000000..b3fe28b9 Binary files /dev/null and b/docs/images/testerhome-login.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..1719125e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,36 @@ + +HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架,只需编写维护一份 `YAML/JSON` 脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。 + +此文档适用于全新发布的 `HttpRunner 2.x` 版本,`1.x` 版本的使用文档请查看[历史链接][httprunner1]。 + +## 设计理念 + +- 充分复用优秀的开源项目,不追求重复造轮子,而是将强大的轮子组装成战车 +- 遵循 `约定大于配置` 的准则,在框架功能中融入自动化测试最佳工程实践 +- 追求投入产出比,一份投入即可实现多种测试需求 + +## 核心特性 + +- 继承 [Requests][Requests] 的全部特性,轻松实现 HTTP(S) 的各种测试需求 +- 采用 `YAML/JSON` 的形式描述测试场景,保障测试用例描述的统一性和可维护性 +- 借助辅助函数(debugtalk.py),在测试脚本中轻松实现复杂的动态计算逻辑 +- 支持完善的测试用例分层机制,充分实现测试用例的复用 +- 测试前后支持完善的 hook 机制 +- 响应结果支持丰富的校验机制 +- 基于 HAR 实现接口录制和用例生成功能([har2case][har2case]) +- 结合 [Locust][Locust] 框架,无需额外的工作即可实现分布式性能测试 +- 执行方式采用 CLI 调用,可与 Jenkins 等持续集成工具完美结合 +- 测试结果统计报告简洁清晰,附带详尽统计信息和日志记录 +- 极强的可扩展性,轻松实现二次开发和 Web 平台化 + +## 更多信息 + +关注 HttpRunner 的微信公众号,第一时间获得最新资讯。 + +![](./assets/qrcode.jpg) + + +[httprunner1]: https://v1.httprunner.org/ +[Requests]: http://docs.python-requests.org/en/master/ +[Locust]: http://locust.io/ +[har2case]: https://github.com/HttpRunner/har2case \ No newline at end of file diff --git a/docs/js/slardar.js b/docs/js/slardar.js new file mode 100644 index 00000000..f6ea9150 --- /dev/null +++ b/docs/js/slardar.js @@ -0,0 +1,11 @@ + +(function(i,s,o,g,r,a,m){i["SlardarMonitorObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date;a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,"script","https://i.snssdk.com/slardar/sdk.js?bid=httprunner","Slardar"); +window.Slardar('config', { + sampleRate: 1, + bid: 'httprunner', + ignoreAjax: [], + ignoreStatic: [], + hookFetch: true, + enableSizeStats: true +}); +window.Slardar('send', 'pageview'); \ No newline at end of file diff --git a/docs/prepare/dot-env.md b/docs/prepare/dot-env.md new file mode 100644 index 00000000..35fefaad --- /dev/null +++ b/docs/prepare/dot-env.md @@ -0,0 +1,112 @@ + +## 环境变量的作用 + +在自动化测试中,有时需要借助环境变量实现某些特定的目的,常见的场景包括: + +- 切换测试环境 +- 切换测试配置 +- 存储敏感数据(从[信息安全](/prepare/security/)的角度出发) + +## 设置环境变量 + +### 在终端中预设环境变量 + +使用环境变量之前,需要先在系统中设置环境变量名称和值,传统的方式为使用 export 命令(Windows系统中使用 set 命令): + +```bash +$ export UserName=admin +$ echo $UserName +admin +$ export Password=123456 +$ echo $Password +123456 +``` + +然后,在程序中就可以对系统中的环境变量进行读取。 + +```bash +$ python +>>> import os +>>> os.environ["UserName"] +'admin' +``` + +### 通过 .env 文件设置环境变量 + +除了这种方式,HttpRunner 还借鉴了 pipenv [加载 `.env` 的方式][pipenv_load_env]。 + +默认情况下,在自动化测试项目的根目录中,创建 `.env` 文件,并将敏感数据信息放置到其中,存储采用 `name=value` 的格式: + +```bash +$ cat .env +UserName=admin +Password=123456 +PROJECT_KEY=ABCDEFGH +``` + +同时,`.env` 文件不应该添加到代码仓库中,建议将 `.env` 加入到 `.gitignore` 中。 + +HttpRunner 运行时,会自动将 `.env` 文件中的内容加载到运行时(RunTime)的环境变量中,然后在运行时中就可以对环境变量进行读取了。 + +若需加载不位于自动化项目根目录中的 `.env`,或者其它名称的 `.env` 文件(例如 `production.env`),可以采用 `--dot-env-path` 参数指定文件路径: + +```bash +$ hrun /path/to/testcase.yml --dot-env-path /path/to/.env --log-level debug +INFO Loading environment variables from /path/to/.env +DEBUG Loaded variable: UserName +DEBUG Loaded variable: Password +DEBUG Loaded variable: PROJECT_KEY +... +``` + +## 引用环境变量 + +在 HttpRunner 中内置了函数 `environ`(简称 `ENV`),可用于在 YAML/JSON 脚本中直接引用环境变量。 + +```yaml +- test: + name: login + request: + url: http://host/api/login + method: POST + headers: + Content-Type: application/json + json: + username: ${ENV(UserName)} + password: ${ENV(Password)} + validate: + - eq: [status_code, 200] +``` + +若还需对读取的环境变量做进一步处理,则可以在 `debugtalk.py` 通过 Python 内置的函数 `os.environ` 对环境变量进行引用,然后再实现处理逻辑。 + +例如,若发起请求的密码需要先与密钥进行拼接并生成 MD5,那么就可以在 `debugtalk.py` 文件中实现如下函数: + +```python +import os + +def get_encrypt_password(): + raw_passwd = os.environ["Password"] + PROJECT_KEY = os.environ["PROJECT_KEY"]) + password = (raw_passwd + PROJECT_KEY).encode('ascii') + return hmac.new(password, hashlib.sha1).hexdigest() +``` + +然后,在 YAML/JSON 格式的测试用例中,就可以通过`${func()}`的方式引用环境变量的值了。 + +```yaml +- test: + name: login + request: + url: http://host/api/login + method: POST + headers: + Content-Type: application/json + json: + username: ${ENV(UserName)} + password: ${get_encrypt_password()} + validate: + - eq: [status_code, 200] +``` + +[pipenv_load_env]: https://docs.pipenv.org/advanced/#automatic-loading-of-env diff --git a/docs/prepare/parameters.md b/docs/prepare/parameters.md new file mode 100644 index 00000000..fe3d8f7a --- /dev/null +++ b/docs/prepare/parameters.md @@ -0,0 +1,581 @@ +## 介绍 + +在自动化测试中,经常会遇到如下场景: + +- 测试搜索功能,只有一个搜索输入框,但有 10 种不同类型的搜索关键字; +- 测试账号登录功能,需要输入用户名和密码,按照等价类划分后有 20 种组合情况。 + +这里只是随意找了两个典型的例子,相信大家都有遇到过很多类似的场景。总结下来,就是在我们的自动化测试脚本中存在参数,并且我们需要采用不同的参数去运行。 + +经过概括,参数基本上分为两种类型: + +- 单个独立参数:例如前面的第一种场景,我们只需要变换搜索关键字这一个参数 +- 多个具有关联性的参数:例如前面的第二种场景,我们需要变换用户名和密码两个参数,并且这两个参数需要关联组合 + +然后,对于参数而言,我们可能具有一个参数列表,在脚本运行时需要按照不同的规则去取值,例如顺序取值、随机取值、循环取值等等。 + +这就是典型的参数化和数据驱动。 + +如需了解 HttpRunner 参数化数据驱动机制的实现原理和技术细节,可前往阅读[《HttpRunner 实现参数化数据驱动机制》](http://debugtalk.com/post/httprunner-data-driven/)。 + +## 测试用例集(testsuite)准备 + +从 2.0.0 版本开始,HttpRunner 不再支持在测试用例文件中进行参数化配置;参数化的功能需要在 testsuite 中实现。变更的目的是让测试用例(testcase)的概念更纯粹,关于测试用例和测试用例集的概念定义,详见[《测试用例组织》](/prepare/parameters/)。 + +参数化机制需要在测试用例集(testsuite)中实现。如需实现数据驱动机制,需要创建一个 testsuite,在 testsuite 中引用测试用例,并定义参数化配置。 + +测试用例集(testsuite)的格式如下所示: + +```yaml +config: + name: testsuite description + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + + testcase2_name: + testcase: /path/to/testcase2 +``` + +需要注意的是,testsuite 和 testcase 的格式存在较大区别,详见[《测试用例组织》](/prepare/testcase-structure/)。 + + +## 参数配置概述 + +如需对某测试用例(testcase)实现参数化数据驱动,需要使用 `parameters` 关键字,定义参数名称并指定数据源取值方式。 + +参数名称的定义分为两种情况: + +- 独立参数单独进行定义; +- 多个参数具有关联性的参数需要将其定义在一起,采用短横线(`-`)进行连接。 + +数据源指定支持三种方式: + +- 在 YAML/JSON 中直接指定参数列表:该种方式最为简单易用,适合参数列表比较小的情况 +- 通过内置的 parameterize(可简写为P)函数引用 CSV 文件:该种方式需要准备 CSV 数据文件,适合数据量比较大的情况 +- 调用 debugtalk.py 中自定义的函数生成参数列表:该种方式最为灵活,可通过自定义 Python 函数实现任意场景的数据驱动机制,当需要动态生成参数列表时也需要选择该种方式 + +三种方式可根据实际项目需求进行灵活选择,同时支持多种方式的组合使用。假如测试用例中定义了多个参数,那么测试用例在运行时会对参数进行笛卡尔积组合,覆盖所有参数组合情况。 + +使用方式概览如下: + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + user_agent: ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + user_id: ${P(user_id.csv)} + username-password: ${get_account(10)} +``` + + +## 参数配置详解 + +将参数名称定义和数据源指定方式进行组合,共有 6 种形式。现分别针对每一类情况进行详细说明。 + +### 独立参数 & 直接指定参数列表 + +对于参数列表比较小的情况,最简单的方式是直接在 YAML/JSON 中指定参数列表内容。 + +例如,对于独立参数 `user_id`,参数列表为 `[1001, 1002, 1003, 1004]`,那么就可以按照如下方式进行配置: + +```yaml +config: + name: testcase description + +testcases: + create user: + testcase: demo-quickstart-6.yml + parameters: + user_id: [1001, 1002, 1003, 1004] +``` + +进行该配置后,测试用例在运行时就会对 user_id 实现数据驱动,即分别使用 `[1001, 1002, 1003, 1004]` 四个值运行测试用例。 + +<details> +<summary>点击查看运行日志</summary> + +```text +$ hrun docs/data/demo-quickstart-7.json +INFO Start to run testcase: create user 1001 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 8.95 ms, response_length: 46 bytes + +. +/api/users/1001 +INFO POST http://127.0.0.1:5000/api/users/1001 +INFO status_code: 201, response_time(ms): 3.02 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.021s + +OK +INFO Start to run testcase: create user 1002 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 2.78 ms, response_length: 46 bytes + +. +/api/users/1002 +INFO POST http://127.0.0.1:5000/api/users/1002 +INFO status_code: 201, response_time(ms): 2.84 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.007s + +OK +INFO Start to run testcase: create user 1003 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 2.92 ms, response_length: 46 bytes + +. +/api/users/1003 +INFO POST http://127.0.0.1:5000/api/users/1003 +INFO status_code: 201, response_time(ms): 5.56 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.011s + +OK +INFO Start to run testcase: create user 1004 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 5.25 ms, response_length: 46 bytes + +. +/api/users/1004 +INFO POST http://127.0.0.1:5000/api/users/1004 +INFO status_code: 201, response_time(ms): 7.02 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.016s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548518757.html +``` + +</details> + +可以看出,测试用例总共运行了 4 次,并且每次运行时都是采用的不同 user_id。 + +### 关联参数 & 直接指定参数列表 + +对于具有关联性的多个参数,例如 username 和 password,那么就可以按照如下方式进行配置: + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + username-password: + - ["user1", "111111"] + - ["user2", "222222"] + - ["user3", "333333"] +``` + +进行该配置后,测试用例在运行时就会对 username 和 password 实现数据驱动,即分别使用 `{"username": "user1", "password": "111111"}`、`{"username": "user2", "password": "222222"}`、`{"username": "user3", "password": "333333"}` 运行 3 次测试,并且保证参数值总是成对使用。 + +### 独立参数 & 引用 CSV 文件 + +对于已有参数列表,并且数据量比较大的情况,比较适合的方式是将参数列表值存储在 CSV 数据文件中。 + +对于 CSV 数据文件,需要遵循如下几项约定的规则: + +- CSV 文件中的第一行必须为参数名称,从第二行开始为参数值,每个(组)值占一行; +- 若同一个 CSV 文件中具有多个参数,则参数名称和数值的间隔符需实用英文逗号; +- 在 YAML/JSON 文件引用 CSV 文件时,文件路径为基于项目根目录(debugtalk.py 所在路径)的相对路径。 + +例如,user_id 的参数取值范围为 1001~2000,那么我们就可以创建 user_id.csv,并且在文件中按照如下形式进行描述。 + +```csv +user_id +1001 +1002 +... +1999 +2000 +``` + +然后在 YAML/JSON 测试用例文件中,就可以通过内置的 `parameterize`(可简写为 `P`)函数引用 CSV 文件。 + +假设项目的根目录下有 data 文件夹,user_id.csv 位于其中,那么 user_id.csv 的引用描述如下: + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + user_id: ${P(data/user_id.csv)} +``` + +即 `P` 函数的参数(CSV 文件路径)是相对于项目根目录的相对路径。当然,这里也可以使用 CSV 文件在系统中的绝对路径,不过这样的话在项目路径变动时就会出现问题,因此推荐使用相对路径的形式。 + +### 关联参数 & 引用 CSV 文件 + +对于具有关联性的多个参数,例如 username 和 password,那么就可以创建 [account.csv](/data/account.csv),并在文件中按照如下形式进行描述。 + +```csv +username,password +test1,111111 +test2,222222 +test3,333333 +``` + +然后在 YAML/JSON 测试用例文件中,就可以通过内置的 `parameterize`(可简写为 `P`)函数引用 CSV 文件。 + +假设项目的根目录下有 data 文件夹,account.csv 位于其中,那么 account.csv 的引用描述如下: + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + username-password: ${P(data/account.csv)} +``` + +需要说明的是,在 parameters 中指定的参数名称必须与 CSV 文件中第一行的参数名称一致,顺序可以不一致,参数个数也可以不一致。 + +例如,在 [account.csv](/data/account.csv) 文件中可以包含多个参数,username、password、phone、age: + +```csv +username,password,phone,age +test1,111111,18600000001,21 +test2,222222,18600000002,22 +test3,333333,18600000003,23 +``` + +而在 YAML/JSON 测试用例文件中指定参数时,可以只使用部分参数,并且参数顺序无需与 CSV 文件中参数名称的顺序一致。 + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + phone-username: ${P(account.csv)} +``` + +### 独立参数 & 引用自定义函数 + +对于没有现成参数列表,或者需要更灵活的方式动态生成参数的情况,可以通过在 debugtalk.py 中自定义函数生成参数列表,并在 YAML/JSON 引用自定义函数的方式。 + +例如,若需对 user_id 进行参数化数据驱动,参数取值范围为 1001~1004,那么就可以在 debugtalk.py 中定义一个函数,返回参数列表。 + +```python +def get_user_id(): + return [ + {"user_id": 1001}, + {"user_id": 1002}, + {"user_id": 1003}, + {"user_id": 1004} + ] +``` + +然后,在 YAML/JSON 的 parameters 中就可以通过调用自定义函数的形式来指定数据源。 + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + user_id: ${get_user_id()} +``` + +另外,通过函数的传参机制,还可以实现更灵活的参数生成功能,在调用函数时指定需要生成的参数个数。 + +### 关联参数 & 引用自定义函数 + +对于具有关联性的多个参数,实现方式也类似。 + +例如,在 debugtalk.py 中定义函数 get_account,生成指定数量的账号密码参数列表。 + +```python +def get_account(num): + accounts = [] + for index in range(1, num+1): + accounts.append( + {"username": "user%s" % index, "password": str(index) * 6}, + ) + + return accounts +``` + +那么在 YAML/JSON 的 parameters 中就可以调用自定义函数生成指定数量的参数列表。 + +```yaml +config: + name: "demo" + +testcases: + testcase1_name: + testcase: /path/to/testcase1 + parameters: + username-password: ${get_account(10)} +``` + +> 需要注意的是,在自定义函数中,生成的参数列表必须为 `list of dict` 的数据结构,该设计主要是为了与 CSV 文件的处理机制保持一致。 + + +## 参数化运行 + +完成以上参数定义和数据源准备工作之后,参数化运行与普通测试用例的运行完全一致。 + +采用 hrun 命令运行自动化测试: + +```bash +$ hrun tests/data/demo_parameters.yml +``` + +采用 locusts 命令运行性能测试: + +```bash +$ locusts -f tests/data/demo_parameters.yml +``` + +区别在于,自动化测试时遍历一遍后会终止执行,性能测试时每个并发用户都会循环遍历所有参数。 + +## 案例演示 + +假设我们有一个获取 token 的[测试用例](/data/demo-testcase-get-token.yml)。 + +<details> +<summary>点击查看 YAML 测试用例</summary> +```yaml +- config: + name: get token + base_url: http://127.0.0.1:5000 + variables: + device_sn: ${gen_random_string(15)} + os_platform: 'ios' + app_version: '2.8.6' + +- test: + name: get token with $device_sn, $os_platform, $app_version + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: $app_version + device_sn: $device_sn + os_platform: $os_platform + json: + sign: ${get_sign($device_sn, $os_platform, $app_version)} + method: POST + url: /api/get-token + extract: + token: content.token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] +``` +</details> + +如果我们需要使用 device_sn、app_version 和 os_platform 这三个参数来进行参数化数据驱动,那么就可以创建一个 [testsuite](/data/demo-parameters-get-token.yml),并且进行参数化配置。 + +```yaml +config: + name: get token with parameters + +testcases: + get token with $user_agent, $app_version, $os_platform: + testcase: demo-testcase-get-token.yml + parameters: + user_agent: ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + app_version: ${P(app_version.csv)} + os_platform: ${get_os_platform()} +``` + +其中,`user_agent` 使用了直接指定参数列表的形式。 + +[app_version](/data/app_version.csv) 通过 CSV 文件进行参数配置,对应的文件内容为: + +```csv +app_version +2.8.5 +2.8.6 +``` + +os_platform 使用自定义函数的形式生成参数列表,对应的函数内容为: + +```python +def get_os_platform(): + return [ + {"os_platform": "ios"}, + {"os_platform": "android"} + ] +``` + +那么,经过笛卡尔积组合,应该总共有 `3*2*2=12` 种参数组合情况。 + + +<details> +<summary>点击查看完整运行日志</summary> +```text +$ hrun docs/data/demo-parameters-get-token.yml +INFO Start to run testcase: get token with iOS/10.1, 2.8.5, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 10.66 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.026s + +OK +INFO Start to run testcase: get token with iOS/10.1, 2.8.5, android +get token with PBJda7SXM2ReWlu, android, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 3.03 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.004s + +OK +INFO Start to run testcase: get token with iOS/10.1, 2.8.6, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 10.76 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.012s + +OK +INFO Start to run testcase: get token with iOS/10.1, 2.8.6, android +get token with PBJda7SXM2ReWlu, android, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 4.49 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.006s + +OK +INFO Start to run testcase: get token with iOS/10.2, 2.8.5, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 4.39 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.006s + +OK +INFO Start to run testcase: get token with iOS/10.2, 2.8.5, android +get token with PBJda7SXM2ReWlu, android, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 4.04 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.005s + +OK +INFO Start to run testcase: get token with iOS/10.2, 2.8.6, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 3.44 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.004s + +OK +INFO Start to run testcase: get token with iOS/10.2, 2.8.6, android +get token with PBJda7SXM2ReWlu, android, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 4.03 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.005s + +OK +INFO Start to run testcase: get token with iOS/10.3, 2.8.5, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 5.14 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.008s + +OK +INFO Start to run testcase: get token with iOS/10.3, 2.8.5, android +get token with PBJda7SXM2ReWlu, android, 2.8.5 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 7.62 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.010s + +OK +INFO Start to run testcase: get token with iOS/10.3, 2.8.6, ios +get token with PBJda7SXM2ReWlu, ios, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 4.88 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.006s + +OK +INFO Start to run testcase: get token with iOS/10.3, 2.8.6, android +get token with PBJda7SXM2ReWlu, android, 2.8.6 +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 5.41 ms, response_length: 46 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.008s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1551950193.html +``` +</details> diff --git a/docs/prepare/project-structure.md b/docs/prepare/project-structure.md new file mode 100644 index 00000000..efbc5e9e --- /dev/null +++ b/docs/prepare/project-structure.md @@ -0,0 +1,30 @@ + +## 文件类型说明 + +在 HttpRunner 自动化测试项目中,主要存在如下几类文件: + +- `YAML/JSON`(必须):测试用例文件,存储接口测试相关信息 +- `debugtalk.py`(可选):存储项目中逻辑运算辅助函数 + - 该文件存在时,将作为项目根目录定位标记,其所在目录即被视为项目工程根目录 + - 该文件不存在时,运行测试的所在路径(`CWD`)将被视为项目工程根目录 + - 测试用例文件中的相对路径(例如`.csv`)均需基于项目工程根目录 + - 运行测试后,测试报告文件夹(`reports`)会生成在项目工程根目录 +- `.env`(可选):存储项目环境变量,通常用于存储项目敏感信息 +- `.csv`(可选):项目数据文件,用于进行数据驱动 +- `reports`:默认生成测试报告的存储文件夹 + +## 项目文件结构 + +对于接口数比较少,或者测试场景比较简单的项目,组织测试用例时无需分层。在此种情况下,项目文件的目录结构没有任何要求,在项目中只需要一堆 `YAML/JSON` 文件即可,每一个文件单独对应一条测试用例;根据需要,项目中可能还会有 `debugtalk.py`、`.env`等文件。 + +推荐的项目文件目录结构示例如下: + +```bash +$ tree demo -a +demo +├── .env +├── debugtalk.py +├── reports +├── testcase1.yml +└── testcase2.json +``` diff --git a/docs/prepare/record.md b/docs/prepare/record.md new file mode 100644 index 00000000..7834b39e --- /dev/null +++ b/docs/prepare/record.md @@ -0,0 +1,38 @@ + +为了简化测试用例的编写工作,HttpRunner 实现了测试用例生成的功能,对应的转换工具为一个独立的项目:[har2case][har2case]。 + +简单来说,就是当前主流的抓包工具和浏览器都支持将抓取得到的数据包导出为标准通用的 HAR 格式(HTTP Archive),然后 HttpRunner 实现了将 HAR 格式的数据包转换为`YAML/JSON`格式的测试用例文件的功能。 + +## 获取 HAR 数据包 + +在转换生成测试用例之前,需要先将抓取得到的数据包导出为 HAR 格式的文件。在`Charles Proxy`中的操作方式为,选中需要转换的接口(可多选或全选),点击右键,在悬浮的菜单目录中点击【Export...】,格式选择`HTTP Archive(.har)`后保存即可;假设我们保存的文件名称为 demo.har。 + +![](../images/charles-export.jpg) + +![](../images/charles-save-har.jpg) + +## 转换生成测试用例 + +然后,在命令行终端中运行 har2case 命令,即可将 demo.har 转换为 HttpRunner 的测试用例文件。 + +使用 `har2case` 转换脚本时默认转换为 JSON 格式。 + +```bash +$ har2case docs/data/demo-quickstart.har +INFO:root:Start to generate testcase. +INFO:root:dump testcase to JSON format. +INFO:root:Generate JSON testcase successfully: docs/data/demo-quickstart.json +``` + +加上 `-2y`/`--to-yml` 参数后转换为 YAML 格式。 + +```bash +$ har2case docs/data/demo-quickstart.har -2y +INFO:root:Start to generate testcase. +INFO:root:dump testcase to YAML format. +INFO:root:Generate YAML testcase successfully: docs/data/demo-quickstart.yml +``` + +两种格式完全等价,YAML 格式更简洁,JSON 格式支持的工具更丰富,大家可根据个人喜好进行选择。 + +[har2case]: https://github.com/HttpRunner/har2case \ No newline at end of file diff --git a/docs/prepare/request-hook.md b/docs/prepare/request-hook.md new file mode 100644 index 00000000..6b268040 --- /dev/null +++ b/docs/prepare/request-hook.md @@ -0,0 +1,145 @@ +## 概述 + +HttpRunner 从 `1.4.5` 版本开始实现了全新的 hook 机制,可以在请求前和请求后调用钩子函数。 + +## 调用 hook 函数 + +hook 机制分为两个层级: + +- 测试用例层面(testcase) +- 测试步骤层面(teststep) + +### 测试用例层面(testcase) + +在 YAML/JSON 测试用例的 `config` 中新增关键字 `setup_hooks` 和 `teardown_hooks`。 + +- setup_hooks: 在整个用例开始执行前触发 hook 函数,主要用于准备工作。 +- teardown_hooks: 在整个用例结束执行后触发 hook 函数,主要用于测试后的清理工作。 + +```yaml +- config: + name: basic test with httpbin + request: + base_url: http://127.0.0.1:3458/ + setup_hooks: + - ${hook_print(setup)} + teardown_hooks: + - ${hook_print(teardown)} +``` + +### 测试步骤层面(teststep) + +在 YAML/JSON 测试步骤的 `test` 中新增关键字 `setup_hooks` 和 `teardown_hooks`。 + +- setup_hooks: 在 HTTP 请求发送前执行 hook 函数,主要用于准备工作;也可以实现对请求的 request 内容进行预处理。 +- teardown_hooks: 在 HTTP 请求发送后执行 hook 函数,主要用于测试后的清理工作;也可以实现对响应的 response 进行修改,例如进行加解密等处理。 + +```json +"test": { + "name": "get token with $user_agent, $os_platform, $app_version", + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "app_version": "$app_version", + "os_platform": "$os_platform", + "user_agent": "$user_agent" + }, + "json": { + "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}" + } + }, + "validate": [ + {"eq": ["status_code", 200]} + ], + "setup_hooks": [ + "${setup_hook_prepare_kwargs($request)}", + "${setup_hook_httpntlmauth($request)}" + ], + "teardown_hooks": [ + "${teardown_hook_sleep_N_secs($response, 2)}" + ] +} +``` + +## 编写 hook 函数 + +hook 函数的定义放置在项目的 `debugtalk.py` 中,在 YAML/JSON 中调用 hook 函数仍然是采用 `${func($a, $b)}` 的形式。 + +对于测试用例层面的 hook 函数,与 YAML/JSON 中自定义的函数完全相同,可通过自定义参数传参的形式来实现灵活应用。 + +```python +def hook_print(msg): + print(msg) +``` + +对于单个测试用例层面的 hook 函数,除了可传入自定义参数外,还可以传入与当前测试用例相关的信息,包括请求的 `$request` 和响应的 `$response`,用于实现更复杂场景的灵活应用。 + +### setup_hooks + +在测试步骤层面的 setup_hooks 函数中,除了可传入自定义参数外,还可以传入 `$request`,该参数对应着当前测试步骤 request 的全部内容。因为 request 是可变参数类型(dict),因此该函数参数为引用传递,当我们需要对请求参数进行预处理时尤其有用。 + +e.g. + +```python +def setup_hook_prepare_kwargs(request): + if request["method"] == "POST": + content_type = request.get("headers", {}).get("content-type") + if content_type and "data" in request: + # if request content-type is application/json, request data should be dumped + if content_type.startswith("application/json") and isinstance(request["data"], (dict, list)): + request["data"] = json.dumps(request["data"]) + + if isinstance(request["data"], str): + request["data"] = request["data"].encode('utf-8') + +def setup_hook_httpntlmauth(request): + if "httpntlmauth" in request: + from requests_ntlm import HttpNtlmAuth + auth_account = request.pop("httpntlmauth") + request["auth"] = HttpNtlmAuth( + auth_account["username"], auth_account["password"]) +``` + +通过上述的 `setup_hook_prepare_kwargs` 函数,可以实现根据请求方法和请求的 Content-Type 来对请求的 data 进行加工处理;通过 `setup_hook_httpntlmauth` 函数,可以实现 HttpNtlmAuth 权限授权。 + +### teardown_hooks + +在测试步骤层面的 teardown_hooks 函数中,除了可传入自定义参数外,还可以传入 `$response`,该参数对应着当前请求的响应实例(requests.Response)。 + +e.g. + +```python +def teardown_hook_sleep_N_secs(response, n_secs): + """ sleep n seconds after request + """ + if response.status_code == 200: + time.sleep(0.1) + else: + time.sleep(n_secs) +``` + +通过上述的 `teardown_hook_sleep_N_secs` 函数,可以根据接口响应的状态码来进行不同时间的延迟等待。 + +另外,在 teardown_hooks 函数中还可以对 response 进行修改。当我们需要先对响应内容进行处理(例如加解密、参数运算),再进行参数提取(extract)和校验(validate)时尤其有用。 + +例如在下面的测试步骤中,在执行测试后,通过 teardown_hooks 函数将响应结果的状态码和 headers 进行了修改,然后再进行了校验。 + +```yaml +- test: + name: alter response + request: + url: /headers + method: GET + teardown_hooks: + - ${alter_response($response)} + validate: + - eq: ["status_code", 500] + - eq: ["headers.content-type", "html/text"] +``` + +```python +def alter_response(response): + response.status_code = 500 + response.headers["Content-Type"] = "html/text" +``` diff --git a/docs/prepare/security.md b/docs/prepare/security.md new file mode 100644 index 00000000..2ad9b3b2 --- /dev/null +++ b/docs/prepare/security.md @@ -0,0 +1,19 @@ + +## 背景 + +很多时候项目代码在运行时需要使用到账号、密码、key等敏感数据信息,但是从信息安全的角度考虑,我们是不能将这些敏感数据提交到代码仓库的,主要原因有两个: + +- 加强权限管控:参与项目的开发人员可能会有很多,大家都有读取代码仓库的权限,但是像 key 这类极度敏感的信息不应该所有人都有权限获取; +- 减少代码泄漏的危害性:假如代码出现泄漏,敏感数据信息不应该也同时泄漏。 + +## 解决方案 + +那代码部署到服务器或 Jenkins 执行机后,运行时要使用到这些敏感数据信息,该怎么操作呢? + +推荐的操作方式为: + +- 对服务器进行权限管控,只有运维人员(或者核心开发人员)才有登录服务器的权限; +- 运维人员(或者核心开发人员):在运行的机器上将敏感数据设置到系统的环境变量中; +- 普通开发人员:只需要知道敏感信息的变量名称,在代码中通过读取环境变量的方式获取敏感数据。 + +存储敏感数据(设置环境变量)和使用敏感数据(引用环境变量)的具体方法,可参考[环境变量](/prepare/dot-env/)使用说明文档。 diff --git a/docs/prepare/testcase-layer.md b/docs/prepare/testcase-layer.md new file mode 100644 index 00000000..eaf31df8 --- /dev/null +++ b/docs/prepare/testcase-layer.md @@ -0,0 +1,272 @@ + + +## 测试用例分层模型 + +在自动化测试领域,自动化测试用例的可维护性是极其重要的因素,直接关系到自动化测试能否持续有效地在项目中开展。 + +概括来说,测试用例分层机制的核心是将接口定义、测试步骤、测试用例、测试场景进行分离,单独进行描述和维护,从而尽可能地减少自动化测试用例的维护成本。 + +逻辑关系图如下所示: + +![](../images/testcase-layer.png) + +同时,强调如下几点核心概念: + +- 测试用例(testcase)应该是完整且独立的,每条测试用例应该是都可以独立运行的 +- 测试用例是测试步骤(teststep)的 `有序` 集合,每一个测试步骤对应一个 API 的请求描述 +- 测试用例集(testsuite)是测试用例的 `无序` 集合,集合中的测试用例应该都是相互独立,不存在先后依赖关系的;如果确实存在先后依赖关系,那就需要在测试用例中完成依赖的处理 + +如果对于上述第三点感觉难以理解,不妨看下上图中的示例: + +- testcase1 依赖于 testcase2,那么就可以在测试步骤(teststep12)中对 testcase2 进行引用,然后 testcase1 就是完整且可独立运行的; +- 在 testsuite 中,testcase1 与 testcase2 相互独立,运行顺序就不再有先后依赖关系了。 + +## 分层描述详解 + +理解了测试用例分层模型,接下来我们再来看下在分层模型下,接口、测试用例、测试用例集的描述形式。 + +### 接口定义(API) + +为了更好地对接口描述进行管理,推荐使用独立的文件对接口描述进行存储,即每个文件对应一个接口描述。 + +接口定义描述的主要内容包括:**name**、variables、**request**、base_url、validate 等,形式如下: + +```yaml +name: get headers +base_url: http://httpbin.org +variables: + expected_status_code: 200 +request: + url: /headers + method: GET +validate: + - eq: ["status_code", $expected_status_code] + - eq: [content.headers.Host, "httpbin.org"] +``` + +其中,name 和 request 部分是必须的,request 中的描述形式与 [requests.request](http://docs.python-requests.org/en/master/api/) 完全相同。 + +另外,API 描述需要尽量保持完整,做到可以单独运行。如果在接口描述中存在变量引用的情况,可在 variables 中对参数进行定义。通过这种方式,可以很好地实现单个接口的调试。 + +```bash +$ hrun api/get_headers.yml +INFO Start to run testcase: get headers +headers +INFO GET http://httpbin.org/headers +INFO status_code: 200, response_time(ms): 477.32 ms, response_length: 157 bytes + +. + +---------------------------------------------------------------------- +Ran 1 test in 0.478s + +OK +``` + +### 测试用例(testcase) + +#### 引用接口定义 + +有了接口的定义描述后,我们编写测试场景时就可以直接引用接口定义了。 + +在测试步骤(teststep)中,可通过 `api` 字段引用接口定义,引用方式为对应 API 文件的路径,绝对路径或相对路径均可。推荐使用相对路径,路径基准为项目根目录,即 `debugtalk.py` 所在的目录路径。 + +```yaml +- config: + name: "setup and reset all." + variables: + user_agent: 'iOS/10.3' + device_sn: "TESTCASE_SETUP_XXX" + os_platform: 'ios' + app_version: '2.8.6' + base_url: "http://127.0.0.1:5000" + verify: False + output: + - session_token + +- test: + name: get token (setup) + api: api/get_token.yml + variables: + user_agent: 'iOS/10.3' + device_sn: $device_sn + os_platform: 'ios' + app_version: '2.8.6' + extract: + - session_token: content.token + validate: + - eq: ["status_code", 200] + - len_eq: ["content.token", 16] + +- test: + name: reset all users + api: api/reset_all.yml + variables: + token: $session_token +``` + +若需要控制或改变接口定义中的参数值,可在测试步骤中指定 variables 参数,覆盖 API 中的 variables 实现。 + +同样地,在测试步骤中定义 validate 后,也会与 API 中的 validate 合并覆盖。因此推荐的做法是,在 API 定义中的 validate 只描述最基本的校验项,例如 status_code,对于与业务逻辑相关的更多校验项,在测试步骤的 validate 中进行描述。 + +#### 引用测试用例 + +在测试用例的测试步骤中,除了可以引用接口定义,还可以引用其它测试用例。通过这种方式,可以在避免重复描述的同时,解决测试用例的依赖关系,从而保证每个测试用例都是独立可运行的。 + +在测试步骤(teststep)中,可通过 `testcase` 字段引用其它测试用例,引用方式为对应测试用例文件的路径,绝对路径或相对路径均可。推荐使用相对路径,路径基准为项目根目录,即 `debugtalk.py` 所在的目录路径。 + +例如,在上面的测试用例("setup and reset all.")中,实现了对获取 token 功能的测试;同时,在很多其它功能中都会依赖于获取 token 的功能,如果将该功能的测试步骤脚本拷贝到其它功能的测试用例中,那么就会存在大量重复,当需要对该部分进行修改时就需要修改所有地方,显然不便于维护。 + +比较好的做法是,在其它功能的测试用例(如创建用户)中,引用获取 token 功能的测试用例(testcases/setup.yml)作为一个测试步骤,从而创建用户("create user and check result.")这个测试用例也变得独立可运行了。 + +```yaml +- config: + name: "create user and check result." + id: create_user + base_url: "http://127.0.0.1:5000" + variables: + uid: 9001 + device_sn: "TESTCASE_CREATE_XXX" + output: + - session_token + +- test: + name: setup and reset all (override) for $device_sn. + testcase: testcases/setup.yml + output: + - session_token + +- test: + name: create user and check result. + variables: + token: $session_token + testcase: testcases/deps/check_and_create.yml +``` + +### 测试用例集(testsuite) + +当测试用例数量比较多以后,为了方便管理和实现批量运行,通常需要使用测试用例集来对测试用例进行组织。 + +在前文的测试用例分层模型中也强调了,测试用例集(testsuite)是测试用例的 `无序` 集合,集合中的测试用例应该都是相互独立,不存在先后依赖关系的;如果确实存在先后依赖关系,那就需要在测试用例中完成依赖的处理。 + +因为是 `无序` 集合,因此测试用例集的描述形式会与测试用例有些不同,在每个测试用例集文件中,第一层级存在两类字段: + +- config: 测试用例集的总体配置参数 +- testcases: 值为字典结构(无序),key 为测试用例的名称,value 为测试用例的内容;在引用测试用例时也可以指定 variables,实现对引用测试用例中 variables 的覆盖。 + +#### 非参数化场景 + +```yaml +config: + name: create users with uid + variables: + device_sn: ${gen_random_string(15)} + var_a: ${gen_random_string(5)} + var_b: $var_a + base_url: "http://127.0.0.1:5000" + +testcases: + create user 1000 and check result.: + testcase: testcases/create_user.yml + variables: + uid: 1000 + var_c: ${gen_random_string(5)} + var_d: $var_c + + create user 1001 and check result.: + testcase: testcases/create_user.yml + variables: + uid: 1001 + var_c: ${gen_random_string(5)} + var_d: $var_c +``` + +#### 参数化场景(parameters) + +对于参数化场景,可通过 parameters 实现,描述形式如下所示。 + +```yaml +config: + name: create users with parameters + variables: + device_sn: ${gen_random_string(15)} + base_url: "http://127.0.0.1:5000" + +testcases: + create user $uid and check result for $device_sn.: + testcase: testcases/create_user.yml + variables: + uid: 1000 + device_sn: TESTSUITE_XXX + parameters: + uid: [101, 102, 103] + device_sn: [TESTSUITE_X1, TESTSUITE_X2] +``` + +参数化后,parameters 中的变量将采用笛卡尔积组合形成参数列表,依次覆盖 variables 中的参数,驱动测试用例的运行。 + +## 文件目录结构管理 && 脚手架工具 + +在对测试用例文件进行组织管理时,对于文件的存储位置均没有要求和限制,在引用时只需要指定对应的文件路径即可。但从约定大于配置的角度,最好是按照推荐的文件夹名称进行存储管理,并可通过子目录实现项目模块分类管理。 + +推荐的方式汇总如下: + +- `debugtalk.py` 放置在项目根目录下,假设为 `PRJ_ROOT_DIR` +- `.env` 放置在项目根目录下,路径为 `PRJ_ROOT_DIR/.env` +- 接口定义(API)放置在 `PRJ_ROOT_DIR/api/` 目录下 +- 测试用例(testcase)放置在 `PRJ_ROOT_DIR/testcases/` 目录下 +- 测试用例集(testsuite)文件必须放置在 `PRJ_ROOT_DIR/testsuites/` 目录下 +- data 文件夹:存储参数化文件,或者项目依赖的文件,路径为 `PRJ_ROOT_DIR/data/` +- reports 文件夹:存储 HTML 测试报告,生成路径为 `PRJ_ROOT_DIR/reports/` + +目录结构如下所示: + +```bash +$ tree tests +tests +├── .env +├── data +│ ├── app_version.csv +│ └── account.csv +├── api +│ ├── create_user.yml +│ ├── get_headers.yml +│ ├── get_token.yml +│ ├── get_user.yml +│ └── reset_all.yml +├── debugtalk.py +├── testcases +│ ├── create_user.yml +│ ├── deps +│ │ └── check_and_create.yml +│ └── setup.yml +└── testsuites + ├── create_users.yml + └── create_users_with_parameters.yml +``` + +**项目脚手架** + +同时,在 `HttpRunner` 中实现了一个脚手架工具,可以快速创建项目的目录结构。该想法来源于 `Django` 的 `django-admin.py startproject project_name`。 + +使用方式也与 `Django` 类似,只需要通过 `--startproject` 指定新项目的名称即可。 + +```bash +$ hrun --startproject demo +Start to create new project: demo +CWD: /Users/debugtalk/MyProjects/examples + +created folder: demo +created folder: demo/api +created folder: demo/testcases +created folder: demo/testsuites +created folder: demo/reports +created file: demo/debugtalk.py +created file: demo/.env +``` + + +## 相关参考 + +- [《HttpRunner 的测试用例分层机制(已过期)》](/post/HttpRunner-testcase-layer) +- 测试用例分层详细示例:[HttpRunner/tests](https://github.com/HttpRunner/HttpRunner/tree/master/tests) diff --git a/docs/prepare/testcase-structure.md b/docs/prepare/testcase-structure.md new file mode 100644 index 00000000..04c27980 --- /dev/null +++ b/docs/prepare/testcase-structure.md @@ -0,0 +1,178 @@ + +## YAML & JSON + +HttpRunner 的测试用例支持两种文件格式:YAML 和 JSON。 + +JSON 和 YAML 格式的测试用例完全等价,包含的信息内容也完全相同。 + +- 对于新手来说,推荐使用 JSON 格式,虽然描述形式上稍显累赘,但是不容易出错(大多编辑器都具有 JSON 格式的检测功能);同时,HttpRunner 也内置了 JSON 格式正确性检测和样式美化功能,详情可查看[《Validate & Prettify》](/testcase//validate-pretty.md)。 +- 对于熟悉 YAML 格式的人来说,编写维护 YAML 格式的测试用例会更简洁,但前提是要保证 YAML 格式没有语法错误。 + +对于两种格式的展示差异,可以对比查看 [demo-quickstart-6.json](/data/demo-quickstart-6.json) 和 [demo-quickstart-6.yml](/data/demo-quickstart-6.yml) 获取初步的印象。 + +后面为了更清晰的描述,统一采用 JSON 格式作为示例。 + +## 测试用例结构 + +![testcase-structure](../images/testcase-structure.png) + +在 HttpRunner 中,测试用例组织主要基于三个概念: + +- 测试用例集(testsuite):对应一个文件夹,包含单个或多个测试用例(`YAML/JSON`)文件 +- 测试用例(testcase):对应一个 `YAML/JSON` 文件,包含单个或多个测试步骤 +- 测试步骤(teststep):对应 `YAML/JSON` 文件中的一个 `test`,描述单次接口测试的全部内容,包括发起接口请求、解析响应结果、校验结果等 + +对于单个 `YAML/JSON` 文件来说,数据存储结构为 `list of dict` 的形式,其中可能包含一个全局配置项(config)和若干个测试步骤(test)。 + +具体地: + +- config:作为整个测试用例的全局配置项 +- test:对应单个测试步骤(teststep),测试用例存在顺序关系,运行时将从前往后依次运行各个测试步骤 + + +对应的 JSON 格式如下所示: + +```json +[ + { + "config": {...} + }, + { + "test": {...} + }, + { + "test": {...} + } +] +``` + +## 变量空间(context)作用域 + +在测试用例内部,HttpRunner 划分了两层变量空间作用域(context)。 + +- config:作为整个测试用例的全局配置项,作用域为整个测试用例; +- test:测试步骤的变量空间(context)会继承或覆盖 config 中定义的内容; + - 若某变量在 config 中定义了,在某 test 中没有定义,则该 test 会继承该变量 + - 若某变量在 config 和某 test 中都定义了,则该 test 中使用自己定义的变量值 +- 各个测试步骤(test)的变量空间相互独立,互不影响; +- 如需在多个测试步骤(test)中传递参数值,则需要使用 extract 关键字,并且只能从前往后传递 + + + + +## config + +```json +"config": { + "name": "testcase description", + "parameters": [ + {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, + {"app_version": "${P(app_version.csv)}"}, + {"os_platform": "${get_os_platform()}"} + ], + "variables": [ + {"user_agent": "iOS/10.3"}, + {"device_sn": "${gen_random_string(15)}"}, + {"os_platform": "ios"} + ], + "request": { + "base_url": "http://127.0.0.1:5000", + "headers": { + "Content-Type": "application/json", + "device_sn": "$device_sn" + } + }, + "output": [ + "token" + ] +} +``` + + Key | required? | format | descrption + --- | --------- | ------ | ---------- + name | Yes | string | 测试用例的名称,在测试报告中将作为标题 + variables | No | list of dict | 定义的全局变量,作用域为整个用例 + parameters | No | list of dict | 全局参数,用于实现数据化驱动,作用域为整个用例 + request | No | dict | request 的公共参数,作用域为整个用例;常用参数包括 base_url 和 headers + +**request** + + Key | required? | format | descrption +---------|----------|---------|--------- + base_url | No | string | 测试用例请求 URL 的公共 host,指定该参数后,test 中的 url 可以只描述 path 部分 + headers | No | dict | request 中 headers 的公共参数,作用域为整个用例 + output | No | list | 整个用例输出的参数列表,可输出的参数包括公共的 variable 和 extract 的参数; 在 log-level 为 debug 模式下,会在 terminal 中打印出参数内容 + +## test + +```json +"test": { + "name": "get token with $user_agent, $os_platform, $app_version", + "request": { + "url": "/api/get-token", + "method": "POST", + "headers": { + "app_version": "$app_version", + "os_platform": "$os_platform", + "user_agent": "$user_agent" + }, + "json": { + "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}" + }, + "extract": [ + {"token": "content.token"} + ], + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]} + ], + "setup_hooks": [], + "teardown_hooks": [] + } +} +``` + + Key | required? | format | descrption + --- | --------- | ------ | ---------- + name | Yes | string | 测试步骤的名称,在测试报告中将作为测试步骤的名称 + request | Yes | dict | HTTP 请求的详细内容;可用参数详见 [python-requests][1] 官方文档 + variables | No | list of dict | 测试步骤中定义的变量,作用域为当前测试步骤 + extract | No | list | 从当前 HTTP 请求的响应结果中提取参数,并保存到参数变量中(例如`token`),后续测试用例可通过`$token`的形式进行引用 + validate | No | list | 测试用例中定义的结果校验项,作用域为当前测试用例,用于实现对当前测试用例运行结果的校验 + setup_hooks | No | list | 在 HTTP 请求发送前执行 hook 函数,主要用于准备工作 + teardown_hooks | No | list | 在 HTTP 请求发送后执行 hook 函数,主要用户测试后的清理工作 + +**extract** + +支持多种提取方式: + +- 响应结果为 JSON 结构,可采用`.`运算符的方式,例如`headers.Content-Type`、`content.success`; +- 响应结果为 text/html 结构,可采用正则表达式的方式,例如`blog-motto\">(.*)</h2>` +- 详情可阅读[《ApiTestEngine,不再局限于API的测试》][2] + +**validate** + +支持两种格式: + +- `{"comparator_name": [check_item, expect_value]}` +- `{"check": check_item, "comparator": comparator_name, "expect": expect_value}` + +**hooks** + +setup_hooks 函数放置于 debugtalk.py 中,并且必须包含三个参数: + +- method: 请求方法,e.g. GET, POST, PUT +- url: 请求 URL +- kwargs: request 的参数字典 + +teardown_hooks 函数放置于 debugtalk.py 中,并且必须包含一个参数: + +- resp_obj: requests.Response 实例 + +关于 `setup_hooks` 和 `teardown_hooks` 的更多内容,请参考[《hook 机制》][3]。 + + +[1]: http://docs.python-requests.org/en/master/api/#main-interface +[2]: http://debugtalk.com/post/apitestengine-not-only-about-json-api/ +[3]: ../../prepare/request-hook/ diff --git a/docs/prepare/validate-pretty.md b/docs/prepare/validate-pretty.md new file mode 100644 index 00000000..d01b74b8 --- /dev/null +++ b/docs/prepare/validate-pretty.md @@ -0,0 +1,74 @@ + +HttpRunner 从 `1.3.1` 版本开始,支持对 JSON 格式测试用例的内容进行格式正确性检测和样式美化功能。 + +## JSON 格式正确性检测 + +若需对 JSON 格式用例文件的内容进行正确性检测,可使用 `--validate` 参数。 + +可指定单个 JSON 用例文件路径。 + +```bash +$ hrun --validate docs/data/demo-quickstart.json +Start to validate JSON file: docs/data/demo-quickstart.json +OK +``` + +也可指定多个 JSON 用例文件路径。 + +```bash +$ hrun --validate docs/data/demo-quickstart.json docs/data/demo-quickstart.yml docs/data/demo-quickstart-0.json +Start to validate JSON file: docs/data/demo-quickstart.json +OK +WARNING Only JSON file format can be validated, skip docs/data/demo-quickstart.yml +Start to validate JSON file: docs/data/demo-quickstart-0.json +OK +``` + +如上所示,当传入的文件后缀不是`.json`,HttpRunner 会打印 WARNING 信息,并跳过检测。 + +若 JSON 文件格式正确,则打印 OK。 + +若 JSON 文件格式存在异常,则打印详细的报错信息,精确到错误在文件中出现的行和列。 + +```bash +$ hrun --validate docs/data/demo-quickstart.json +Start to validate JSON file: docs/data/demo-quickstart.json +Expecting ',' delimiter: line 5 column 13 (char 82) +``` + +## JSON 格式美化 + +与 YAML 格式不同,JSON 格式不强制要求缩进和换行,这有点类似于 C 语言和 Python 语言的差异。 + +例如,`demo-quickstart.json`文件也可以改写为如下形式。 + +```json +[{"config": {"name": "testcase description","variables": [],"request": {"base_url": "","headers": {"User-Agent": "python-requests/2.18.4"}}}},{"test": {"name": "/api/get-token","request": {"url": "http://127.0.0.1:5000/api/get-token","headers": {"device_sn": "FwgRiO7CNA50DSU","user_agent": "iOS/10.3","os_platform": "ios","app_version": "2.8.6","Content-Type": "application/json"},"method": "POST","json": {"sign": "9c0c7e51c91ae963c833a4ccbab8d683c4a90c98"}},"validate": [{"eq": ["status_code",200]},{"eq": ["headers.Content-Type","application/json"]},{"eq": ["content.success",true]},{"eq": ["content.token","baNLX1zhFYP11Seb"]}]}},{"test": {"name": "/api/users/1000","request": {"url": "http://127.0.0.1:5000/api/users/1000","headers": {"device_sn": "FwgRiO7CNA50DSU","token": "baNLX1zhFYP11Seb","Content-Type": "application/json"},"method": "POST","json": {"name": "user1","password": "123456"}},"validate": [{"eq": ["status_code",201]},{"eq": ["headers.Content-Type","application/json"]},{"eq": ["content.success",true]},{"eq": ["content.msg","user created successfully."]}]}}] +``` + +虽然上面 JSON 格式的测试用例也能正常执行,但测试用例文件的可读性太差,不利于阅读和维护。 + +针对该需求,可使用 `--prettify` 参数对 JSON 格式用例文件进行样式美化。 + +可指定单个 JSON 用例文件路径。 + +```bash +$ hrun --prettify docs/data/demo-quickstart.json +Start to prettify JSON file: docs/data/demo-quickstart.json +success: docs/data/demo-quickstart.pretty.json +``` + +也可指定多个 JSON 用例文件路径。 + +```bash +$ hrun --prettify docs/data/demo-quickstart.json docs/data/demo-quickstart.yml docs/data/demo-quickstart-0.json +WARNING Only JSON file format can be prettified, skip: docs/data/demo-quickstart.yml +Start to prettify JSON file: docs/data/demo-quickstart.json +success: docs/data/demo-quickstart.pretty.json +Start to prettify JSON file: docs/data/demo-quickstart-0.json +success: docs/data/demo-quickstart-0.pretty.json +``` + +如上所示,当传入的文件后缀不是`.json`,HttpRunner 会打印 WARNING 信息,并跳过检测。 + +若转换成功,则打印美化后的文件路径;若 JSON 文件格式存在异常,则打印详细的报错信息,精确到错误在文件中出现的行和列。 diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..4e957ada --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,624 @@ +本文将通过一个简单的示例来展示 HttpRunner 的核心功能使用方法。 + +## 案例介绍 + +该案例作为被测服务,主要有两类接口: + +- 权限校验,获取 token +- 支持 CRUD 操作的 RESTful APIs,所有接口的请求头域中都必须包含有效的 token + +案例的实现形式为 flask 应用服务([api_server.py](data/api_server.py)),启动方式如下: + +```text +$ export FLASK_APP=docs/data/api_server.py +$ export FLASK_ENV=development +$ flask run + * Serving Flask app "docs/data/api_server.py" (lazy loading) + * Environment: development + * Debug mode: on + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: 989-476-348 +``` + +服务启动成功后,我们就可以开始对其进行测试了。 + +## 测试准备 + +### 抓包分析 + +在开始测试之前,我们需要先了解接口的请求和响应细节,而最佳的方式就是采用 `Charles Proxy` 或者 `Fiddler` 这类网络抓包工具进行抓包分析。 + +例如,在本案例中,我们先进行权限校验,然后成功创建一个用户,对应的网络抓包内容如下图所示: + +![](./images/demo-quickstart-http-1.jpg) + +![](./images/demo-quickstart-http-2.jpg) + +通过抓包,我们可以看到具体的接口信息,包括请求的URL、Method、headers、参数和响应内容等内容,基于这些信息,我们就可以开始编写测试用例了。 + +### 生成测试用例 + +为了简化测试用例的编写工作,HttpRunner 实现了测试用例生成的功能。 + +首先,需要将抓取得到的数据包导出为 HAR 格式的文件,假设导出的文件名称为 [demo-quickstart.har](data/demo-quickstart.har)。 + +然后,在命令行终端中运行如下命令,即可将 demo-quickstart.har 转换为 HttpRunner 的测试用例文件。 + +```bash +$ har2case docs/data/demo-quickstart.har -2y +INFO:root:Start to generate testcase. +INFO:root:dump testcase to YAML format. +INFO:root:Generate YAML testcase successfully: docs/data/demo-quickstart.yml +``` + +使用 `har2case` 转换脚本时默认转换为 JSON 格式,加上 `-2y` 参数后转换为 YAML 格式。两种格式完全等价,YAML 格式更简洁,JSON 格式支持的工具更丰富,大家可根据个人喜好进行选择。关于 [har2case][har2case] 的详细使用说明,请查看[《录制生成测试用例》](/prepare/record/)。 + +经过转换,在源 demo-quickstart.har 文件的同级目录下生成了相同文件名称的 YAML 格式测试用例文件 [demo-quickstart.yml](data/demo-quickstart.yml),其内容如下: + +```yaml +- config: + name: testcase description + variables: {} + +- test: + name: /api/get-token + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + app_version: 2.8.6 + device_sn: FwgRiO7CNA50DSU + os_platform: ios + json: + sign: 9c0c7e51c91ae963c833a4ccbab8d683c4a90c98 + method: POST + url: http://127.0.0.1:5000/api/get-token + validate: + - eq: [status_code, 200] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.token, baNLX1zhFYP11Seb] + +- test: + name: /api/users/1000 + request: + headers: + Content-Type: application/json + User-Agent: python-requests/2.18.4 + device_sn: FwgRiO7CNA50DSU + token: baNLX1zhFYP11Seb + json: + name: user1 + password: '123456' + method: POST + url: http://127.0.0.1:5000/api/users/1000 + validate: + - eq: [status_code, 201] + - eq: [headers.Content-Type, application/json] + - eq: [content.success, true] + - eq: [content.msg, user created successfully.] +``` + +现在我们只需要知道如下几点: + +- 每个 YAML/JSON 文件对应一个测试用例(testcase) +- 每个测试用例为一个`list of dict`结构,其中可能包含全局配置项(config)和若干个测试步骤(test) +- `config` 为全局配置项,作用域为整个测试用例 +- `test` 对应单个测试步骤,作用域仅限于本身 + +如上便是 HttpRunner 测试用例的基本结构。 + +关于测试用例的更多内容,请查看[《测试用例结构描述》](/prepare/testcase-structure/)。 + +### 首次运行测试用例 + +测试用例就绪后,我们可以开始调试运行了。 + +为了演示测试用例文件的迭代优化过程,我们先将 demo-quickstart.json 重命名为 [demo-quickstart-0.json](data/demo-quickstart-0.json)(对应的 YAML 格式:[demo-quickstart-0.yml](data/demo-quickstart-0.yml))。 + +运行测试用例的命令为`hrun`,后面直接指定测试用例文件的路径即可。 + +```text +$ hrun docs/data/demo-quickstart-0.yml +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 9.26 ms, response_length: 46 bytes + +ERROR validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail +tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) +ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** +====== request details ====== +url: http://127.0.0.1:5000/api/get-token +method: POST +headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'app_version': '2.8.6', 'device_sn': 'FwgRiO7CNA50DSU', 'os_platform': 'ios'} +json: {'sign': '9c0c7e51c91ae963c833a4ccbab8d683c4a90c98'} +verify: True + +====== response details ====== +status_code: 200 +headers: {'Content-Type': 'application/json', 'Content-Length': '46', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:43:55 GMT'} +body: '{"success": true, "token": "tXGuSQgOCVXcltkz"}' + +F +/api/users/1000 +INFO POST http://127.0.0.1:5000/api/users/1000 +ERROR 403 Client Error: FORBIDDEN for url: http://127.0.0.1:5000/api/users/1000 +ERROR validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +ERROR validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +ERROR validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) +ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** +====== request details ====== +url: http://127.0.0.1:5000/api/users/1000 +method: POST +headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'device_sn': 'FwgRiO7CNA50DSU', 'token': 'baNLX1zhFYP11Seb'} +json: {'name': 'user1', 'password': '123456'} +verify: True + +====== response details ====== +status_code: 403 +headers: {'Content-Type': 'application/json', 'Content-Length': '50', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:43:55 GMT'} +body: '{"success": false, "msg": "Authorization failed!"}' + +F + +====================================================================== +FAIL: test_0000_000 (httprunner.api.TestSequense) +/api/get-token +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test + test_runner.run_test(test_dict) +httprunner.exceptions.ValidationFailure: validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail +tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test + self.fail(str(ex)) +AssertionError: validate: content.token equals baNLX1zhFYP11Seb(str) ==> fail +tXGuSQgOCVXcltkz(str) equals baNLX1zhFYP11Seb(str) + +====================================================================== +FAIL: test_0001_000 (httprunner.api.TestSequense) +/api/users/1000 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test + test_runner.run_test(test_dict) +httprunner.exceptions.ValidationFailure: validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test + self.fail(str(ex)) +AssertionError: validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) + +---------------------------------------------------------------------- +Ran 2 tests in 0.026s + +FAILED (failures=2) +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548513835.html +``` + +非常不幸,两个接口的测试用例均运行失败了。 + +## 优化测试用例 + +从两个测试步骤的报错信息和堆栈信息(Traceback)可以看出,第一个步骤失败的原因是获取的 token 与预期值不一致,第二个步骤失败的原因是请求权限校验失败(403)。 + +接下来我们将逐步进行进行优化。 + +### 调整校验器 + +默认情况下,[har2case][har2case] 生成用例时,若 HTTP 请求的响应内容为 JSON 格式,则会将第一层级中的所有`key-value`转换为 validator。 + +例如上面的第一个测试步骤,生成的 validator 为: + +```json +"validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["headers.Content-Type", "application/json"]}, + {"eq": ["content.success", true]}, + {"eq": ["content.token", "baNLX1zhFYP11Seb"]} +] +``` + +运行测试用例时,就会对上面的各个项进行校验。 + +问题在于,请求`/api/get-token`接口时,每次生成的 token 都会是不同的,因此将生成的 token 作为校验项的话,校验自然就无法通过了。 + +正确的做法是,在测试步骤的 validate 中应该去掉这类动态变化的值。 + +去除该项后,将用例另存为 [demo-quickstart-1.json](data/demo-quickstart-1.json)(对应的 YAML 格式:[demo-quickstart-1.yml](data/demo-quickstart-1.yml))。 + +再次运行测试用例,运行结果如下: + +```text +$ hrun docs/data/demo-quickstart-1.yml +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 6.61 ms, response_length: 46 bytes + +. +/api/users/1000 +INFO POST http://127.0.0.1:5000/api/users/1000 +ERROR 403 Client Error: FORBIDDEN for url: http://127.0.0.1:5000/api/users/1000 +ERROR validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +ERROR validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +ERROR validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) +ERROR ******************************** DETAILED REQUEST & RESPONSE ******************************** +====== request details ====== +url: http://127.0.0.1:5000/api/users/1000 +method: POST +headers: {'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.18.4', 'device_sn': 'FwgRiO7CNA50DSU', 'token': 'baNLX1zhFYP11Seb'} +json: {'name': 'user1', 'password': '123456'} +verify: True + +====== response details ====== +status_code: 403 +headers: {'Content-Type': 'application/json', 'Content-Length': '50', 'Server': 'Werkzeug/0.14.1 Python/3.7.0', 'Date': 'Sat, 26 Jan 2019 14:45:34 GMT'} +body: '{"success": false, "msg": "Authorization failed!"}' + +F + +====================================================================== +FAIL: test_0001_000 (httprunner.api.TestSequense) +/api/users/1000 +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 54, in test + test_runner.run_test(test_dict) +httprunner.exceptions.ValidationFailure: validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/Users/debugtalk/.pyenv/versions/3.6-dev/lib/python3.6/site-packages/httprunner/api.py", line 56, in test + self.fail(str(ex)) +AssertionError: validate: status_code equals 201(int) ==> fail +403(int) equals 201(int) +validate: content.success equals True(bool) ==> fail +False(bool) equals True(bool) +validate: content.msg equals user created successfully.(str) ==> fail +Authorization failed!(str) equals user created successfully.(str) + +---------------------------------------------------------------------- +Ran 2 tests in 0.018s + +FAILED (failures=1) +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548513934.html +``` + +经过修改,第一个测试步骤已经运行成功了,第二个步骤仍然运行失败(403),还是因为权限校验的原因。 + +### 参数关联 + +我们继续查看 [demo-quickstart-1.json](data/demo-quickstart-1.json),会发现第二个测试步骤的请求 headers 中的 token 仍然是硬编码的,即抓包时获取到的值。在我们再次运行测试用例时,这个 token 已经失效了,所以会出现 403 权限校验失败的问题。 + +正确的做法是,我们应该在每次运行测试用例的时候,先动态获取到第一个测试步骤中的 token,然后在后续测试步骤的请求中使用前面获取到的 token。 + +在 HttpRunner 中,支持参数提取(`extract`)和参数引用的功能(`$var`)。 + +在测试步骤(test)中,若需要从响应结果中提取参数,则可使用 `extract` 关键字。extract 的列表中可指定一个或多个需要提取的参数。 + +在提取参数时,当 HTTP 的请求响应结果为 JSON 格式,则可以采用`.`运算符的方式,逐级往下获取到参数值;响应结果的整体内容引用方式为 content 或者 body。 + +例如,第一个接口`/api/get-token`的响应结果为: + +```json +{"success": true, "token": "ZQkYhbaQ6q8UFFNE"} +``` + +那么要获取到 token 参数,就可以使用 content.token 的方式;具体的写法如下: + +```json +"extract": [ + {"token": "content.token"} +] +``` + +其中,token 作为提取后的参数名称,可以在后续使用 `$token` 进行引用。 + +```json +"headers": { + "device_sn": "FwgRiO7CNA50DSU", + "token": "$token", + "Content-Type": "application/json" +} +``` + +修改后的测试用例另存为 [demo-quickstart-2.json](data/demo-quickstart-2.json)(对应的 YAML 格式:[demo-quickstart-2.yml](data/demo-quickstart-2.yml))。 + +再次运行测试用例,运行结果如下: + +```text +$ hrun docs/data/demo-quickstart-2.yml +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 8.32 ms, response_length: 46 bytes + +. +/api/users/1000 +INFO POST http://127.0.0.1:5000/api/users/1000 +INFO status_code: 201, response_time(ms): 3.02 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.019s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548514191.html +``` + +经过修改,第二个测试步骤也运行成功了。 + +### base_url + +虽然测试步骤运行都成功了,但是仍然有继续优化的地方。 + +继续查看 [demo-quickstart-2.json](data/demo-quickstart-2.json),我们会发现在每个测试步骤的 URL 中,都采用的是完整的描述(host+path),但大多数情况下同一个用例中的 host 都是相同的,区别仅在于 path 部分。 + +因此,我们可以将各个测试步骤(test) URL 的 `base_url` 抽取出来,放到全局配置模块(config)中,在测试步骤中的 URL 只保留 PATH 部分。 + +```yaml +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + +- test: + name: get token + request: + url: /api/get-token +``` + +调整后的测试用例另存为 [demo-quickstart-3.json](data/demo-quickstart-3.json)(对应的 YAML 格式:[demo-quickstart-3.yml](data/demo-quickstart-3.yml))。 + +重启 flask 应用服务后再次运行测试用例,所有的测试步骤仍然运行成功。 + +### 变量的申明和引用 + +继续查看 [demo-quickstart-3.json](data/demo-quickstart-3.json),我们会发现测试用例中存在较多硬编码的参数,例如 app_version、device_sn、os_platform、user_id 等。 + +大多数情况下,我们可以不用修改这些硬编码的参数,测试用例也能正常运行。但是为了更好地维护测试用例,例如同一个参数值在测试步骤中出现多次,那么比较好的做法是,将这些参数定义为变量,然后在需要参数的地方进行引用。 + +在 HttpRunner 中,支持变量申明(`variables`)和引用(`$var`)的机制。在 config 和 test 中均可以通过 `variables` 关键字定义变量,然后在测试步骤中可以通过 `$ + 变量名称` 的方式引用变量。区别在于,在 config 中定义的变量为全局的,整个测试用例(testcase)的所有地方均可以引用;在 test 中定义的变量作用域仅局限于当前测试步骤(teststep)。 + +对上述各个测试步骤中硬编码的参数进行变量申明和引用调整后,新的测试用例另存为 [demo-quickstart-4.json](data/demo-quickstart-4.json)(对应的 YAML 格式:[demo-quickstart-4.yml](data/demo-quickstart-4.yml))。 + +重启 flask 应用服务后再次运行测试用例,所有的测试步骤仍然运行成功。 + +### 抽取公共变量 + +查看 [demo-quickstart-4.json](data/demo-quickstart-4.json) 可以看出,两个测试步骤中都定义了 device_sn。针对这类公共的参数,我们可以将其统一定义在 config 的 variables 中,在测试步骤中就不用再重复定义。 + +```yaml +- config: + name: testcase description + base_url: http://127.0.0.1:5000 + variables: + device_sn: FwgRiO7CNA50DSU +``` + +调整后的测试用例见 [demo-quickstart-5.json](data/demo-quickstart-5.json)(对应的 YAML 格式:[demo-quickstart-5.yml](data/demo-quickstart-5.yml))。 + +### 实现动态运算逻辑 + +在 [demo-quickstart-5.yml](data/demo-quickstart-5.yml) 中,参数 device_sn 代表的是设备的 SN 编码,虽然采用硬编码的方式暂时不影响测试用例的运行,但这与真实的用户场景不大相符。 + +假设 device_sn 的格式为 15 长度的字符串,那么我们就可以在每次运行测试用例的时候,针对 device_sn 生成一个 15 位长度的随机字符串。与此同时,sign 字段是根据 headers 中的各个字段拼接后生成得到的 MD5 值,因此在 device_sn 变动后,sign 也应该重新进行计算,否则就会再次出现签名校验失败的问题。 + +然而,HttpRunner 的测试用例都是采用 YAML/JSON 格式进行描述的,在文本格式中如何执行代码运算呢? + +HttpRunner 的实现方式为,支持热加载的插件机制(`debugtalk.py`),可以在 YAML/JSON 中调用 Python 函数。 + +具体地做法,我们可以在测试用例文件的同级或其父级目录中创建一个 debugtalk.py 文件,然后在其中定义相关的函数和变量。 + +例如,针对 device_sn 的随机字符串生成功能,我们可以定义一个 gen_random_string 函数;针对 sign 的签名算法,我们可以定义一个 get_sign 函数。 + +```python +import hashlib +import hmac +import random +import string + +SECRET_KEY = "DebugTalk" + +def gen_random_string(str_len): + random_char_list = [] + for _ in range(str_len): + random_char = random.choice(string.ascii_letters + string.digits) + random_char_list.append(random_char) + + random_string = ''.join(random_char_list) + return random_string + +def get_sign(*args): + content = ''.join(args).encode('ascii') + sign_key = SECRET_KEY.encode('ascii') + sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest() + return sign +``` + +然后,我们在 YAML/JSON 测试用例文件中,就可以对定义的函数进行调用,对定义的变量进行引用了。引用变量的方式仍然与前面讲的一样,采用`$ + 变量名称`的方式;调用函数的方式为`${func($var)}`。 + +例如,生成 15 位长度的随机字符串并赋值给 device_sn 的代码为: + +```json +"variables": [ + {"device_sn": "${gen_random_string(15)}"} +] +``` + +使用 $user_agent、$device_sn、$os_platform、$app_version 根据签名算法生成 sign 值的代码为: + +```json +"json": { + "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}" +} +``` + +对测试用例进行上述调整后,另存为 [demo-quickstart-6.json](data/demo-quickstart-6.json)(对应的 YAML 格式:[demo-quickstart-6.yml](data/demo-quickstart-6.yml))。 + +重启 flask 应用服务后再次运行测试用例,所有的测试步骤仍然运行成功。 + +### 参数化数据驱动 + +> 请确保你使用的 HttpRunner 版本号不低于 2.0.0 + +在 [demo-quickstart-6.yml](data/demo-quickstart-6.yml) 中,user_id 仍然是写死的值,假如我们需要创建 user_id 为 1001~1004 的用户,那我们只能不断地去修改 user_id,然后运行测试用例,重复操作 4 次?或者我们在测试用例文件中将创建用户的 test 复制 4 份,然后在每一份里面分别使用不同的 user_id ? + +很显然,不管是采用上述哪种方式,都会很繁琐,并且也无法应对灵活多变的测试需求。 + +针对这类需求,HttpRunner 支持参数化数据驱动的功能。 + +在 HttpRunner 中,若要采用数据驱动的方式来运行测试用例,需要创建一个文件,对测试用例进行引用,并使用 `parameters` 关键字定义参数并指定数据源取值方式。 + +例如,我们需要在创建用户的接口中对 user_id 进行参数化,参数化列表为 1001~1004,并且取值方式为顺序取值,那么最简单的描述方式就是直接指定参数列表。具体的编写方式为,新建一个测试场景文件 [demo-quickstart-7.yml](data/demo-quickstart-7.yml)(对应的 JSON 格式:[demo-quickstart-7.json](data/demo-quickstart-7.json)),内容如下所示: + +```yaml +config: + name: testcase description + +testcases: + create user: + testcase: demo-quickstart-6.yml + parameters: + user_id: [1001, 1002, 1003, 1004] +``` + +仅需如上配置,针对 user_id 的参数化数据驱动就完成了。 + +重启 flask 应用服务后再次运行测试用例,测试用例运行情况如下所示: + +<details> +<summary>点击查看运行日志</summary> + +```text +$ hrun docs/data/demo-quickstart-7.json +INFO Start to run testcase: create user 1001 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 8.95 ms, response_length: 46 bytes + +. +/api/users/1001 +INFO POST http://127.0.0.1:5000/api/users/1001 +INFO status_code: 201, response_time(ms): 3.02 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.021s + +OK +INFO Start to run testcase: create user 1002 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 2.78 ms, response_length: 46 bytes + +. +/api/users/1002 +INFO POST http://127.0.0.1:5000/api/users/1002 +INFO status_code: 201, response_time(ms): 2.84 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.007s + +OK +INFO Start to run testcase: create user 1003 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 2.92 ms, response_length: 46 bytes + +. +/api/users/1003 +INFO POST http://127.0.0.1:5000/api/users/1003 +INFO status_code: 201, response_time(ms): 5.56 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.011s + +OK +INFO Start to run testcase: create user 1004 +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 5.25 ms, response_length: 46 bytes + +. +/api/users/1004 +INFO POST http://127.0.0.1:5000/api/users/1004 +INFO status_code: 201, response_time(ms): 7.02 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.016s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548518757.html +``` + +</details> + +可以看出,测试用例总共运行了 4 次,并且每次运行时都是采用的不同 user_id。 + +关于参数化数据驱动,这里只描述了最简单的场景和使用方式,如需了解更多,请进一步阅读[《数据驱动使用手册》](/prepare/parameters/)。 + +[har2case]: https://github.com/HttpRunner/har2case + +## 查看测试报告 + +在每次使用 hrun 命令运行测试用例后,均会生成一份 HTML 格式的测试报告。报告文件位于 reports 目录下,文件名称为测试用例的开始运行时间。 + +例如,在运行完 [demo-quickstart-1.json](data/demo-quickstart-1.json) 后,将生成如下形式的测试报告: + +![](./images/report-demo-quickstart-1-overview.jpg) + +![](./images/report-demo-quickstart-1-log2.jpg) + +![](./images/report-demo-quickstart-1-traceback.jpg) + +关于测试报告的详细内容,请查看[《测试报告》](/run-tests/report/)部分。 + +## 总结 + +到此为止,HttpRunner 的核心功能就介绍完了,掌握本文中的功能特性,足以帮助你应对日常项目工作中至少 80% 的自动化测试需求。 + +当然,HttpRunner 不止于此,如需挖掘 HttpRunner 的更多特性,实现更复杂场景的自动化测试需求,可继续阅读后续文档。 diff --git a/docs/related-docs.md b/docs/related-docs.md new file mode 100644 index 00000000..d7d314d9 --- /dev/null +++ b/docs/related-docs.md @@ -0,0 +1,17 @@ + +## 开发笔记 + +- [DebugTalk](http://debugtalk.com/tags/HttpRunner/) + +## 演讲 + +- [MTSC 2018][MTSC2018]: [《大疆互联网的一站式自动化测试解决方案(基于HttpRunner)》][dji-httprunner] +- [PyCon China 2018][PyConChina2018]: [《借助 Python 开源生态打造企业级自动化测试框架(HttpRunner)》][PyCon-HttpRunner] +- [MTSC 2019][MTSC2019]: [《HttpRunner 2.0 技术架构与接口测试应用》][httprunner-2.0] + +[MTSC2018]: https://www.bagevent.com/event/1193113 +[PyConChina2018]: http://cn.pycon.org/2018/city_beijing.html +[dji-httprunner]: /attachments/DJI-HttpRunner.pdf +[PyCon-HttpRunner]: /attachments/PyCon-HttpRunner.pdf +[MTSC2019]: https://testerhome.com/mtsc/2019 +[httprunner-2.0]: /attachments/MTSC2019-HttpRunner-2.0.pdf diff --git a/docs/run-tests/cli.md b/docs/run-tests/cli.md new file mode 100644 index 00000000..e538ef7d --- /dev/null +++ b/docs/run-tests/cli.md @@ -0,0 +1,218 @@ + +HttpRunner 在命令行中启动测试时,通过指定参数,可实现丰富的测试特性控制。 + +```text +$ hrun -h +usage: hrun [-h] [-V] [--log-level LOG_LEVEL] [--log-file LOG_FILE] + [--dot-env-path DOT_ENV_PATH] [--report-template REPORT_TEMPLATE] + [--report-dir REPORT_DIR] [--failfast] [--save-tests] + [--startproject STARTPROJECT] + [--validate [VALIDATE [VALIDATE ...]]] + [--prettify [PRETTIFY [PRETTIFY ...]]] + [testcase_paths [testcase_paths ...]] + +One-stop solution for HTTP(S) testing. + +positional arguments: + testcase_paths testcase file path + +optional arguments: + -h, --help show this help message and exit + -V, --version show version + --log-level LOG_LEVEL + Specify logging level, default is INFO. + --log-file LOG_FILE Write logs to specified file path. + --dot-env-path DOT_ENV_PATH + Specify .env file path, which is useful for keeping + sensitive data. + --report-template REPORT_TEMPLATE + specify report template path. + --report-dir REPORT_DIR + specify report save directory. + --failfast Stop the test run on the first error or failure. + --save-tests Save loaded tests and parsed tests to JSON file. + --startproject STARTPROJECT + Specify new project name. + --validate [VALIDATE [VALIDATE ...]] + Validate JSON testcase format. + --prettify [PRETTIFY [PRETTIFY ...]] + Prettify JSON testcase format. +``` + +## 指定测试用例路径 + +使用 HttpRunner 指定测试用例路径时,支持多种方式。 + +使用 hrun 命令外加单个测试用例文件的路径,运行单个测试用例,并生成一个测试报告文件: + +```text +$ hrun filepath/testcase.yml +``` + +将多个测试用例文件放置到文件夹中,指定文件夹路径可将文件夹下所有测试用例作为测试用例集进行运行,并生成一个测试报告文件: + +```text +$ hrun testcases_folder_path +``` + +## failfast + +默认情况下,HttpRunner 会运行指定用例集中的所有测试用例,并统计测试结果。 + +> 对于某些依赖于执行顺序的测试用例,例如需要先登录成功才能执行后续接口请求的场景,当前面的测试用例执行失败后,后续的测试用例也都必将失败,因此没有继续执行的必要了。 + +若希望测试用例在运行过程中,遇到失败时不再继续运行后续用例,则可通过在命令中添加`--failfast`实现。 + +```text +$ hrun filepath/testcase.yml --failfast +``` + +## 日志级别 + +默认情况下,HttpRunner 运行时的日志级别为`INFO`,只会包含最基本的信息,包括用例名称、请求的URL和Method、响应结果的状态码、耗时和内容大小。 + +```text +$ hrun docs/data/demo-quickstart-6.json +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 9.08 ms, response_length: 46 bytes + +. +/api/users/1548560655759 +INFO POST http://127.0.0.1:5000/api/users/1548560655759 +INFO status_code: 201, response_time(ms): 2.89 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.019s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548560655.html +``` + +若需要查看到更详尽的信息,例如请求的参数和响应的详细内容,可以将日志级别设置为`DEBUG`,即在命令中添加`--log-level debug`。 + +``` +$ hrun docs/data/demo-quickstart-6.json --log-level debug +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +DEBUG request kwargs(raw): {'headers': {'User-Agent': 'python-requests/2.18.4', 'device_sn': 'W5ACRDytKRQJPhC', 'os_platform': 'ios', 'app_version': '2.8.6', 'Content-Type': 'application/json'}, 'json': {'sign': '2e7c3b5d560a1c8a859edcb9c8b0d3f8349abeff'}, 'verify': True} +DEBUG processed request: +> POST http://127.0.0.1:5000/api/get-token +> kwargs: {'headers': {'User-Agent': 'python-requests/2.18.4', 'device_sn': 'W5ACRDytKRQJPhC', 'os_platform': 'ios', 'app_version': '2.8.6', 'Content-Type': 'application/json'}, 'json': {'sign': '2e7c3b5d560a1c8a859edcb9c8b0d3f8349abeff'}, 'verify': True, 'timeout': 120} +DEBUG +================== request details ================== +url : 'http://127.0.0.1:5000/api/get-token' +method : 'POST' +headers : {'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'device_sn': 'W5ACRDytKRQJPhC', 'os_platform': 'ios', 'app_version': '2.8.6', 'Content-Type': 'application/json', 'Content-Length': '52'} +body : b'{"sign": "2e7c3b5d560a1c8a859edcb9c8b0d3f8349abeff"}' + +DEBUG +================== response details ================== +ok : True +url : 'http://127.0.0.1:5000/api/get-token' +status_code : 200 +reason : 'OK' +cookies : {} +encoding : None +headers : {'Content-Type': 'application/json', 'Content-Length': '46', 'Server': 'Werkzeug/0.14.1 Python/3.6.5+', 'Date': 'Sun, 27 Jan 2019 03:45:16 GMT'} +content_type : 'application/json' +json : {'success': True, 'token': 'o6uakmubLrCbpRRS'} + +INFO status_code: 200, response_time(ms): 9.28 ms, response_length: 46 bytes + +DEBUG start to extract from response object. +DEBUG extract: content.token => o6uakmubLrCbpRRS +DEBUG start to validate. +DEBUG extract: status_code => 200 +DEBUG validate: status_code equals 200(int) ==> pass +DEBUG extract: headers.Content-Type => application/json +DEBUG validate: headers.Content-Type equals application/json(str) ==> pass +DEBUG extract: content.success => True +DEBUG validate: content.success equals True(bool) ==> pass +. +/api/users/1548560716736 +INFO POST http://127.0.0.1:5000/api/users/1548560716736 +DEBUG request kwargs(raw): {'headers': {'User-Agent': 'python-requests/2.18.4', 'device_sn': 'W5ACRDytKRQJPhC', 'token': 'o6uakmubLrCbpRRS', 'Content-Type': 'application/json'}, 'json': {'name': 'user1', 'password': '123456'}, 'verify': True} +DEBUG processed request: +> POST http://127.0.0.1:5000/api/users/1548560716736 +> kwargs: {'headers': {'User-Agent': 'python-requests/2.18.4', 'device_sn': 'W5ACRDytKRQJPhC', 'token': 'o6uakmubLrCbpRRS', 'Content-Type': 'application/json'}, 'json': {'name': 'user1', 'password': '123456'}, 'verify': True, 'timeout': 120} +DEBUG +================== request details ================== +url : 'http://127.0.0.1:5000/api/users/1548560716736' +method : 'POST' +headers : {'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'device_sn': 'W5ACRDytKRQJPhC', 'token': 'o6uakmubLrCbpRRS', 'Content-Type': 'application/json', 'Content-Length': '39'} +body : b'{"name": "user1", "password": "123456"}' + +DEBUG +================== response details ================== +ok : True +url : 'http://127.0.0.1:5000/api/users/1548560716736' +status_code : 201 +reason : 'CREATED' +cookies : {} +encoding : None +headers : {'Content-Type': 'application/json', 'Content-Length': '54', 'Server': 'Werkzeug/0.14.1 Python/3.6.5+', 'Date': 'Sun, 27 Jan 2019 03:45:16 GMT'} +content_type : 'application/json' +json : {'success': True, 'msg': 'user created successfully.'} + +INFO status_code: 201, response_time(ms): 2.77 ms, response_length: 54 bytes + +DEBUG start to validate. +DEBUG extract: status_code => 201 +DEBUG validate: status_code equals 201(int) ==> pass +DEBUG extract: headers.Content-Type => application/json +DEBUG validate: headers.Content-Type equals application/json(str) ==> pass +DEBUG extract: content.success => True +DEBUG validate: content.success equals True(bool) ==> pass +DEBUG extract: content.msg => user created successfully. +DEBUG validate: content.msg equals user created successfully.(str) ==> pass +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.022s + +OK +DEBUG No html report template specified, use default. +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548560716.html +``` + +## 保存详细过程数据 + +为了方便定位问题,运行测试时可指定 `--save-tests` 参数,即可将运行过程的中间数据保存为日志文件。 + +```text +$ hrun docs/data/demo-quickstart-6.json --save-tests +dump file: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/docs/data/logs/demo-quickstart-6.loaded.json +dump file: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/docs/data/logs/demo-quickstart-6.parsed.json +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 11.42 ms, response_length: 46 bytes + +. +/api/users/1548560768589 +INFO POST http://127.0.0.1:5000/api/users/1548560768589 +INFO status_code: 201, response_time(ms): 2.8 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.028s + +OK +dump file: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/docs/data/logs/demo-quickstart-6.summary.json +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548560768.html +``` + +日志文件将保存在项目根目录的 `logs` 文件夹中,生成的文件有如下三个(XXX为测试用例名称): + +- `XXX.loaded.json`:测试用例加载后的数据结构内容,加载包括测试用例文件(YAML/JSON)、debugtalk.py、.env 等所有项目文件,例如 [`demo-quickstart-6.loaded.json`](/data/logs/demo-quickstart-6.loaded.json) +- `XXX.parsed.json`:测试用例解析后的数据结构内容,解析内容包括测试用例引用(API/testcase)、变量计算和替换、base_url 拼接等,例如 [`demo-quickstart-6.parsed.json`](/data/logs/demo-quickstart-6.parsed.json) +- `XXX.summary.json`:测试报告生成前的数据结构内容,例如 [`demo-quickstart-6.summary.json`](/data/logs/demo-quickstart-6.summary.json) diff --git a/docs/run-tests/load-test.md b/docs/run-tests/load-test.md new file mode 100644 index 00000000..00448bda --- /dev/null +++ b/docs/run-tests/load-test.md @@ -0,0 +1,157 @@ +HttpRunner 通过复用 [`Locust`][Locust],可以在无需对 YAML/JSON 进行任何修改的情况下,直接运行性能测试。 + +## 原理图 + +![](../images/loadtest-schematic-diagram.jpg) + +## 安装依赖包 + +安装完成 HttpRunner 后,系统中会新增`locusts`命令,但不会同时安装 Locust。 + +在系统中未安装 Locust 的情况下,运行`locusts`命令时会出现如下提示。 + +```bash +$ locusts -V +WARNING Locust is not installed, install first and try again. +install command: pip install locustio +``` + +Locust 的安装方式如下: + +```text +$ pip install locustio +``` + +安装完成后,执行 `locusts -V` 可查看到 Locust 的版本号。 + +```text +$ locusts -V +[2017-08-26 23:45:42,246] bogon/INFO/stdout: Locust 0.8a2 +[2017-08-26 23:45:42,246] bogon/INFO/stdout: +``` + +执行 `locusts -h`,可查看到使用帮助文档。 + +```text +$ locusts -h +Usage: locust [options] [LocustClass [LocustClass2 ... ]] + +Options: + -h, --help show this help message and exit + -H HOST, --host=HOST Host to load test in the following format: + http://10.21.32.33 + --web-host=WEB_HOST Host to bind the web interface to. Defaults to '' (all + interfaces) + -P PORT, --port=PORT, --web-port=PORT + Port on which to run web host + -f LOCUSTFILE, --locustfile=LOCUSTFILE + Python module file to import, e.g. '../other.py'. + Default: locustfile + --csv=CSVFILEBASE, --csv-base-name=CSVFILEBASE + Store current request stats to files in CSV format. + --master Set locust to run in distributed mode with this + process as master + --slave Set locust to run in distributed mode with this + process as slave + --master-host=MASTER_HOST + Host or IP address of locust master for distributed + load testing. Only used when running with --slave. + Defaults to 127.0.0.1. + --master-port=MASTER_PORT + The port to connect to that is used by the locust + master for distributed load testing. Only used when + running with --slave. Defaults to 5557. Note that + slaves will also connect to the master node on this + port + 1. + --master-bind-host=MASTER_BIND_HOST + Interfaces (hostname, ip) that locust master should + bind to. Only used when running with --master. + Defaults to * (all available interfaces). + --master-bind-port=MASTER_BIND_PORT + Port that locust master should bind to. Only used when + running with --master. Defaults to 5557. Note that + Locust will also use this port + 1, so by default the + master node will bind to 5557 and 5558. + --expect-slaves=EXPECT_SLAVES + How many slaves master should expect to connect before + starting the test (only when --no-web used). + --no-web Disable the web interface, and instead start running + the test immediately. Requires -c and -r to be + specified. + -c NUM_CLIENTS, --clients=NUM_CLIENTS + Number of concurrent clients. Only used together with + --no-web + -r HATCH_RATE, --hatch-rate=HATCH_RATE + The rate per second in which clients are spawned. Only + used together with --no-web + -n NUM_REQUESTS, --num-request=NUM_REQUESTS + Number of requests to perform. Only used together with + --no-web + -L LOGLEVEL, --loglevel=LOGLEVEL + Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. + Default is INFO. + --logfile=LOGFILE Path to log file. If not set, log will go to + stdout/stderr + --print-stats Print stats in the console + --only-summary Only print the summary stats + --no-reset-stats Do not reset statistics once hatching has been + completed + -l, --list Show list of possible locust classes and exit + --show-task-ratio print table of the locust classes' task execution + ratio + --show-task-ratio-json + print json data of the locust classes' task execution + ratio + -V, --version show program's version number and exit +``` + +可以看出,`loucsts` 命令与 `locust` 命令的用法基本相同。 + +相比于 `locust` 命令,`loucsts`命令主要存在如下两项差异。 + +## 运行性能测试 + +在 `-f` 参数后面,`loucsts` 命令不仅可以指定 Locust 支持的 Python 文件,同时可以直接指定 YAML/JSON 格式的测试用例文件。在具体实现上,当 `-f` 指定 YAML/JSON 格式的测试用例文件时,会先将其转换为 Python 格式的 locustfile,然后再将 locustfile.py 传给 locust 命令。 + +```bash +$ locusts -f examples/first-testcase.yml +[2017-08-18 17:20:43,915] Leos-MacBook-Air.local/INFO/locust.main: Starting web monitor at *:8089 +[2017-08-18 17:20:43,918] Leos-MacBook-Air.local/INFO/locust.main: Starting Locust 0.8a2 +``` + +执行上述命令后,即完成了 Locust 服务的启动,后续就可以在 Locust 的 Web 管理界面中进行操作了,使用方式与 Locust 完全相同。 + +## 多进程运行模式 + +默认情况下,在 Locust 中如需使用 master-slave 模式启动多个进程(使用多核处理器的能力),只能先启动 master,然后再逐一启动若干个 slave。 + +```text +$ locust -f locustfile.py --master +$ locust -f locustfile.py --slave & +$ locust -f locustfile.py --slave & +$ locust -f locustfile.py --slave & +$ locust -f locustfile.py --slave & +``` + +在 HttpRunner 中,新增实现 `--processes` 参数,可以一次性启动 1 个 master 和多个 salve。若在 `--processes` 参数后没有指定具体的数值,则启动的 slave 个数与机器的 CPU 核数相同。 + +```bash +$ locusts -f examples/first-testcase.yml --processes 4 +[2017-08-26 23:51:47,071] bogon/INFO/locust.main: Starting web monitor at *:8089 +[2017-08-26 23:51:47,075] bogon/INFO/locust.main: Starting Locust 0.8a2 +[2017-08-26 23:51:47,078] bogon/INFO/locust.main: Starting Locust 0.8a2 +[2017-08-26 23:51:47,080] bogon/INFO/locust.main: Starting Locust 0.8a2 +[2017-08-26 23:51:47,083] bogon/INFO/locust.main: Starting Locust 0.8a2 +[2017-08-26 23:51:47,084] bogon/INFO/locust.runners: Client 'bogon_656e0af8e968a8533d379dd252422ad3' reported as ready. Currently 1 clients ready to swarm. +[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_09f73850252ee4ec739ed77d3c4c6dba' reported as ready. Currently 2 clients ready to swarm. +[2017-08-26 23:51:47,084] bogon/INFO/locust.main: Starting Locust 0.8a2 +[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_869f7ed671b1a9952b56610f01e2006f' reported as ready. Currently 3 clients ready to swarm. +[2017-08-26 23:51:47,085] bogon/INFO/locust.runners: Client 'bogon_80a804cda36b80fac17b57fd2d5e7cdb' reported as ready. Currently 4 clients ready to swarm. +``` + +![](../images/locusts-full-speed.jpg) + +Enjoy! + + +[Locust]: http://locust.io/ \ No newline at end of file diff --git a/docs/run-tests/report.md b/docs/run-tests/report.md new file mode 100644 index 00000000..c680424d --- /dev/null +++ b/docs/run-tests/report.md @@ -0,0 +1,113 @@ + +使用 HttpRunner 执行完自动化测试后,会在当前路径的 reports 目录下生成一份 HTML 格式的测试报告。 + +## 默认情况 + +默认情况下,生成的测试报告文件会位于项目根目录的 reports 文件夹中,文件名称为测试开始的时间戳。 + +```bash +$ hrun docs/data/demo-quickstart-6.yml +INFO Start to run testcase: testcase description +/api/get-token +INFO POST http://127.0.0.1:5000/api/get-token +INFO status_code: 200, response_time(ms): 10.05 ms, response_length: 46 bytes + +. +/api/users/1548561170497 +INFO POST http://127.0.0.1:5000/api/users/1548561170497 +INFO status_code: 201, response_time(ms): 2.88 ms, response_length: 54 bytes + +. + +---------------------------------------------------------------------- +Ran 2 tests in 0.034s + +OK +INFO Start to render Html report ... +INFO Generated Html report: /Users/debugtalk/MyProjects/HttpRunner-dev/httprunner-docs-v2x/reports/1548561170.html +``` + +点击查看[测试报告](/data/reports/1548561170.html)。 + +## 默认报告样式 + +在 HttpRunner 中自带了一个 Jinja2 格式的报告模版,默认情况下,生成的报告样式均基于该模版([httprunner/templates/default_report_template.html][default_report])。 + +测试报告形式如下: + +在 Summary 中,会罗列本次测试的整体信息,包括测试开始时间、总运行时长、运行的Python版本和系统环境、运行结果统计数据。 + +![](../images/report-demo-quickstart-1-overview.jpg) + +在 Details 中,会详细展示每一条测试用例的运行结果。 + +点击测试用例对应的 log 按钮,会在弹出框中展示该用例执行的详细数据,包括请求的 headers 和 body、响应的 headers 和 body、校验结果、响应、响应耗时(elapsed)等信息。 + +![](../images/report-demo-quickstart-1-log1.jpg) +![](../images/report-demo-quickstart-1-log2.jpg) + +若测试用例运行不成功(failed/error/skipped),则在该测试用例的 detail 中会出现 traceback 按钮,点击该按钮后,会在弹出框中展示失败的堆栈日志,或者 skipped 的原因。 + +![](../images/report-demo-quickstart-1-traceback.jpg) + +点击查看[测试报告](/data/reports/1548561464.html)。 + +## 自定义 + +除了默认的报告样式,HttpRunner 还支持使用自定义的报告模板。 + +### 编写自定义模板(Jinja2格式) + +自定义模板需要采用 [Jinja2][Jinja2] 的格式,其中可以使用的数据可参考[数据结构示例][summary_data]。 + +例如,我们需要在自定义模板中展示测试结果的统计数据,就可以采用如下方式进行描述: + +```html +<tr> + <th>TOTAL</th> + <th>SUCCESS</th> + <th>FAILED</th> + <th>ERROR</th> + <th>SKIPPED</th> +</tr> +<tr> + <td>{{stat.testsRun}}</td> + <td>{{stat.successes}}</td> + <td>{{stat.failures}}</td> + <td>{{stat.errors}}</td> + <td>{{stat.skipped}}</td> +</tr> +``` + +在自定义报告模板时,可以参考 HttpRunner 的[默认报告模板][default_report],要实现更复杂的模版功能,可参考 [Jinja2][Jinja2] 的使用文档。 + +### 使用自定义模板 + +使用自定义模版时,需要通过 `--report-template` 指定报告模板的路径,然后测试运行完成后,就会采用自定义的模板生成测试报告。 + +```bash +$ hrun docs/data/demo-quickstart-2.yml --report-template /path/to/custom_report_template +... +同上,省略 + +INFO render with html report template: /path/to/custom_report_template +INFO Start to render Html report ... +INFO Generated Html report: reports/1532078874.html +``` + +[Jinja2]: http://jinja.pocoo.org/docs/latest +[default_report]: https://github.com/HttpRunner/HttpRunner/blob/master/httprunner/templates/report_template.html +[summary_data]: /development/#_6 + +### 指定报告生成路径 + +默认情况下,生成的测试报告文件会位于项目根目录的 reports 文件夹中。如需指定生成报告的路径,可以使用 `--report-dir` 参数。 + +```bash +$ hrun docs/data/demo-quickstart-2.yml --dirreport-name /other/path/ +... +同上,省略 + +INFO Start to render Html report ... +INFO Generated Html report: /other/path/1532078874.html +``` diff --git a/docs/sponsors.md b/docs/sponsors.md index 40f13869..5025a18c 100644 --- a/docs/sponsors.md +++ b/docs/sponsors.md @@ -4,7 +4,7 @@ ## 金牌赞助商(Gold Sponsor) -[<img src="./hogwarts.png" alt="霍格沃兹测试学院" width="400">](https://testing-studio.com) +[<img src="./assets/hogwarts.png" alt="霍格沃兹测试学院" width="400">](https://testing-studio.com) > [霍格沃兹测试学院](https://testing-studio.com) 是由测吧(北京)科技有限公司与知名软件测试社区 [TesterHome](https://testerhome.com/) 合作的高端教育品牌。由 BAT 一线**测试大咖执教**,提供**实战驱动**的接口自动化测试、移动自动化测试、性能测试、持续集成与 DevOps 等技术培训,以及测试开发优秀人才内推服务。[点击学习!](https://ke.qq.com/course/348893?flowToken=1014523) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..b90e7be7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,80 @@ +# require mkdocs-material 3.x + +# Project information +site_name: HttpRunner V2.x 中文使用文档 +site_description: HttpRunner V2.x User Documentation +site_author: 'debugtalk' + +# Repository +repo_name: HttpRunner +repo_url: https://github.com/HttpRunner/HttpRunner +edit_uri: "" + +# Copyright +copyright: 'Copyright © 2017 - 2019 debugtalk' + +# Configuration +theme: + name: 'material' + language: 'zh' + palette: + primary: 'indigo' + accent: 'indigo' + font: + text: 'Roboto' + code: 'Roboto Mono' + +# Google Analytics +google_analytics: + - 'UA-114587036-2' + - 'auto' + +# Extensions +markdown_extensions: + - admonition + - codehilite: + guess_lang: false + - toc: + permalink: true + +# extra +extra: + search: + language: 'jp' + social: + - type: globe + link: https://debugtalk.com + - type: 'github' + link: 'https://github.com/httprunner' + +# index pages +nav: + - 介绍: index.md + - 安装说明: Installation.md + - 快速上手: quickstart.md + - 基础概念: + - 名词解释: concept/nominal.md + - 测试准备: + - 录制生成用例: prepare/record.md + - 项目文件组织: prepare/project-structure.md + - 测试用例组织: prepare/testcase-structure.md + - hook机制: prepare/request-hook.md + - 环境变量: prepare/dot-env.md + - 测试用例分层: prepare/testcase-layer.md + - 参数化数据驱动: prepare/parameters.md + - Validate & Prettify: prepare/validate-pretty.md + - 信息安全: prepare/security.md + - 测试执行: + - 运行测试(CLI): run-tests/cli.md + - 测试报告: run-tests/report.md + - 性能测试: run-tests/load-test.md + - 开发扩展: + - Pipline: development/architecture.md + - 基础库调用: development/dev-api.md + - FAQ: FAQ.md + - 实践案例: + - TesterHome 登录: examples/testerhome-login.md + - 相关资料: related-docs.md + +extra_javascript: + - 'js/slardar.js' \ No newline at end of file