├── InterAutoTest_W ├── data │ ├── __init__.py │ ├── testdata.xlsx │ └── testlogin.yml ├── logs │ └── __init__.py ├── utils │ ├── __init__.py │ ├── YamlUtil.py │ ├── AssertUtil.py │ ├── ExcelUtil.py │ ├── RequestsUtil.py │ ├── LogUtil.py │ └── MysqlUtil.py ├── common │ ├── __init__.py │ ├── ExcelConfig.py │ ├── ExcelData.py │ └── Base.py ├── config │ ├── __init__.py │ ├── db_conf.yml │ ├── conf.yml │ └── Conf.py ├── report │ ├── __init__.py │ ├── assets │ │ └── style.css │ └── report.html ├── testcase │ ├── __init__.py │ ├── t_yaml │ │ ├── __init__.py │ │ ├── yaml_demo.py │ │ └── data.yml │ ├── t_allure │ │ ├── __init__.py │ │ └── allure_demo.py │ ├── t_excel │ │ ├── __init__.py │ │ ├── testdata.xlsx │ │ └── excel_demo.py │ ├── t_pytest │ │ ├── __init__.py │ │ ├── pytest_one.py │ │ ├── pytest_more.py │ │ ├── pytest_class.py │ │ ├── pytest_demo.py │ │ ├── pytest_func.py │ │ └── report │ │ │ ├── assets │ │ │ └── style.css │ │ │ └── report.html │ ├── test_log │ │ ├── __init__.py │ │ ├── log_demo.py │ │ └── log_file_demo.py │ ├── T_requests.py │ ├── test_login.py │ ├── test_mall.py │ └── test_excel_case.py ├── run.py ├── pytest.ini └── .vscode │ └── settings.json ├── 美多商城-接口文档.PDF ├── image └── 代码测试结构.jpg ├── requirement.txt ├── LICENSE ├── .gitignore └── README.md /InterAutoTest_W/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/logs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_yaml/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_allure/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_excel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_log/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /美多商城-接口文档.PDF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengfeiZhou/testpy/HEAD/美多商城-接口文档.PDF -------------------------------------------------------------------------------- /image/代码测试结构.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengfeiZhou/testpy/HEAD/image/代码测试结构.jpg -------------------------------------------------------------------------------- /InterAutoTest_W/data/testdata.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengfeiZhou/testpy/HEAD/InterAutoTest_W/data/testdata.xlsx -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/T_requests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests 3 | r = requests.get('http://www.baidu.com') 4 | print(r) -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_excel/testdata.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chengfeiZhou/testpy/HEAD/InterAutoTest_W/testcase/t_excel/testdata.xlsx -------------------------------------------------------------------------------- /InterAutoTest_W/config/db_conf.yml: -------------------------------------------------------------------------------- 1 | db_1: 2 | host: "211.103.136.242" 3 | port: 7090 4 | user: "test" 5 | password: "test123456" 6 | database: "meiduo" 7 | charset: "utf8" 8 | -------------------------------------------------------------------------------- /InterAutoTest_W/config/conf.yml: -------------------------------------------------------------------------------- 1 | BASE: 2 | log_level: 'debug' 3 | log_extension: '.log' 4 | 5 | test: 6 | url: "http://211.103.136.242:8064" 7 | case_file: "testdata.xlsx" 8 | case_sheet: 0 -------------------------------------------------------------------------------- /InterAutoTest_W/run.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import pytest 4 | from config import Conf 5 | 6 | if __name__ == "__main__": 7 | report_path = Conf._report_path() 8 | pytest.main(['-s', '--alluredir', report_path+'/reslut']) -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_yaml/yaml_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | 1. 创建yaml文件 4 | 2. 读取yaml文件 5 | 3. 输出yaml文件 6 | """ 7 | import yaml 8 | 9 | with open('./data.yml', 'r') as f: 10 | r = yaml.safe_load(f) 11 | 12 | print(r) 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /InterAutoTest_W/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # addopts = -s --html=./report/report.html --reruns 3 --reruns-delay=2 3 | # addopts = -s --html=./report/report.html 4 | 5 | # allure setting 6 | addopts = -s 7 | 8 | 9 | testpaths = testcases 10 | python_files = test*.py 11 | python_classes = Test* 12 | python_functions = test* 13 | 14 | -------------------------------------------------------------------------------- /InterAutoTest_W/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "testcase" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.nosetestsEnabled": false, 7 | "python.testing.pytestEnabled": true, 8 | "python.pythonPath": "C:\\Develop\\Python\\Anaconda3\\envs\\testpy\\python.exe" 9 | } -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_log/log_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 1.导入logging包 3 | import logging 4 | # 2.设置配置信息 5 | logging.basicConfig(level=logging.INFO, format="%(asctime)s-%(name)s-%(levelname)s-%(message)s") 6 | # 3. 定义日志名称:get_logger 7 | logger = logging.getLogger('log_demo') 8 | # 4. info,debug 9 | logger.info('info') 10 | logger.debug('debug') 11 | logger.warning('warning') 12 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/pytest_one.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | 1. 创建类和测试方法 4 | 2. 创建数据 5 | 3. 创建参数化 6 | 4.运行 7 | """ 8 | import pytest 9 | 10 | class TestClass(): 11 | data_list = ['xiaoming', 'xiaohong'] 12 | 13 | @pytest.mark.parametrize('name',data_list) 14 | def test_a(self, name): 15 | print('test_a') 16 | print(name) 17 | assert 1 18 | 19 | 20 | if __name__ == "__main__": 21 | pytest.main(['-s','pytest_one.py']) 22 | -------------------------------------------------------------------------------- /InterAutoTest_W/data/testlogin.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 'case_name': "success login" 4 | 'url': 'http://211.103.136.242:8064/authorizations/' 5 | 'data': 6 | 'username': "python" 7 | 'password': "12345678" 8 | 'expect': "'username': 'python', 'user_id': 1" 9 | --- 10 | 'case_name': "failure login" 11 | 'url': 'http://211.103.136.242:8064/authorizations/' 12 | 'data': 13 | 'username': "test123456" 14 | 'password': "1231111" 15 | 'expect': "'username': 'python', 'user_id': 1" 16 | 17 | 18 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/pytest_more.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | 1. 创建类和测试方法 4 | 2. 创建数据 5 | 3. 创建参数化 6 | 4.运行 7 | """ 8 | import pytest 9 | 10 | class TestClass(): 11 | data_list = [('xiaoming', '12345'),('xiaohong', '56789')] 12 | 13 | @pytest.mark.parametrize(('name','psw'),data_list) 14 | def test_a(self, name,psw): 15 | print('test_a') 16 | print(name,psw) 17 | assert 1 18 | 19 | 20 | if __name__ == "__main__": 21 | pytest.main(['-s','pytest_more.py']) -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | allure-pytest==2.8.6 2 | allure-python-commons==2.8.6 3 | atomicwrites==1.3.0 4 | attrs==19.3.0 5 | certifi==2019.9.11 6 | chardet==3.0.4 7 | colorama==0.4.1 8 | idna==2.8 9 | more-itertools==7.2.0 10 | packaging==19.2 11 | pluggy==0.13.1 12 | py==1.8.0 13 | PyMySQL==0.9.3 14 | pyparsing==2.4.5 15 | pytest==5.3.1 16 | pytest-html==2.0.1 17 | pytest-metadata==1.8.0 18 | pytest-rerunfailures==8.0 19 | PyYAML==5.1.2 20 | requests==2.22.0 21 | six==1.13.0 22 | urllib3==1.25.7 23 | wcwidth==0.1.7 24 | wincertstore==0.2 25 | xlrd==1.2.0 26 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_yaml/data.yml: -------------------------------------------------------------------------------- 1 | # name: "test_yaml" 2 | # result: "success" 3 | 4 | # - 12 5 | # - 32 6 | # - 33 7 | 8 | 9 | # person1: 10 | # name: xiaoming 11 | # age: 18 12 | # person2: 13 | # name: xiaohong 14 | # age: 16 15 | 16 | # person: 17 | # - "a" 18 | # - "b" 19 | # - c 20 | 21 | 22 | # - 23 | # - 1 24 | # - 2 25 | # - 3 26 | # - "b" 27 | # - c 28 | # - 29 | # - 6 30 | # - 7 31 | # - 8 32 | 33 | - 34 | name: xiaoming 35 | age: 18 36 | - 2 37 | - 3 38 | - 39 | name: xiaohong 40 | age: 16 41 | 42 | 43 | -------------------------------------------------------------------------------- /InterAutoTest_W/common/ExcelConfig.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 定义类 4 | # 定义列属性 5 | 6 | class DataConfig(): 7 | # 用例属性: 8 | case_id = "用例ID" 9 | case_model = "模块" 10 | case_name = "接口名称" 11 | url = "请求URL" 12 | pre_exec = "前置条件" 13 | method = "请求类型" 14 | params_type = "请求参数类型" 15 | params = "请求参数" 16 | expect_result = "预期结果" 17 | actual_result = "实际结果" 18 | beizhu = "备注" 19 | is_run = "是否运行" 20 | headers = "headers" 21 | cookies = "cookies" 22 | code = "status_code" 23 | db_verify = "数据库验证" 24 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/pytest_class.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | """ 4 | 1.定义类; 5 | 2.创建测试方法test开头 6 | 3.创建setup_class, teardown_class 7 | 4.运行查看结果 8 | """ 9 | import pytest 10 | 11 | class TestClass(): 12 | def test_a(self): 13 | print('test_a') 14 | 15 | def test_b(self): 16 | print('test_b') 17 | 18 | def setup_class(self): 19 | print('------setup_class------') 20 | 21 | def teardown_class(self): 22 | print('------teardown_class------') 23 | 24 | if __name__ == "__main__": 25 | pytest.main(['-s', 'pytest_class.py']) -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/pytest_demo.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | # 1. 创建简单的测试方法 4 | # 2. pytest运行 5 | # 2.1 idea中直接执行 6 | # 2.2 命令行执行 7 | import pytest 8 | 9 | # 创建普通的方法 10 | def func(x): 11 | return x+1 12 | 13 | 14 | # 创建pytest断言的方法 15 | def test_a(): 16 | print("---test_a---") 17 | assert func(3) == 5 # 断言失败 18 | 19 | # 使用装饰器,控制单个测试用例的运行情况 20 | @pytest.mark.flaky(reruns=3, reruns_delay=2) 21 | def test_b(): 22 | print('---test_b---') 23 | assert func(3) == 4 # 断言成功 24 | 25 | # 代码直接执行 26 | if __name__ == "__main__": 27 | pytest.main(["pytest_demo.py"]) 28 | 29 | 30 | # 命令行执行: 31 | # pytest pytest_demo.py 32 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_excel/excel_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ 4 | 1. 导入包, xlrd(python自带) 5 | 2. 创建workbook对象 6 | 3. sheet对象 7 | 4. 获取行数和列数 8 | 5. 读取每行的内容 9 | 6. 读取每列的内容 10 | 7. 读取固定列的内容 11 | """ 12 | import xlrd 13 | book = xlrd.open_workbook('./testdata.xlsx') 14 | # 获取表的两种方式: 15 | # 索引 16 | # sheet = book.sheet_by_index(0) 17 | # 名称 18 | sheet = book.sheet_by_name('美多商城接口测试') 19 | 20 | rows = sheet.nrows # 行数 21 | cols = sheet.ncols # 列数 22 | print(f"rows:{rows}, cols:{cols}") 23 | 24 | # 获取每行数据 25 | for r in range(rows): 26 | r_values = sheet.row_values(r) 27 | print(r_values) 28 | 29 | # 获取每列数据 30 | for c in range(cols): 31 | c_values = sheet.col_values(c) 32 | print(c_values) 33 | 34 | # 读取固定列的内容 35 | v = sheet.cell(1,2) 36 | print(v) -------------------------------------------------------------------------------- /InterAutoTest_W/utils/YamlUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 1. 创建类 4 | # 2. 初始化,文件是否存在 5 | # 3. yaml读取 6 | 7 | import os 8 | import yaml 9 | 10 | class YamlReader(): 11 | def __init__(self, yaml_p): 12 | if os.path.exists(yaml_p): 13 | self.yaml_p = yaml_p 14 | else: 15 | raise FileNotFoundError("文件不存在") 16 | self._data = None 17 | self._data_all = None 18 | 19 | def data(self): 20 | # 读取单个文档 21 | if not self._data: 22 | with open(self.yaml_p, 'r') as f: 23 | self._data = yaml.safe_load(f) 24 | return self._data 25 | 26 | def data_all(self): 27 | # 读取单个文档 28 | if not self._data: 29 | with open(self.yaml_p, 'r') as f: 30 | self._data_all = list(yaml.safe_load_all(f)) 31 | return self._data_all 32 | 33 | 34 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_log/log_file_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | 5 | # 1. 设置logger名称 6 | logger = logging.getLogger('log_file_demo') 7 | # 2. 设置log级别 8 | logger.setLevel(logging.INFO) 9 | # 3. 创建handler, 用于输出控制台或写入文件 10 | # 输出到控制台 11 | fh_stream = logging.StreamHandler() 12 | # 写入文件 13 | fh_file = logging.FileHandler('./test.log') 14 | # 4. 设置日志级别 15 | fh_stream.setLevel(logging.INFO) 16 | fh_file.setLevel(logging.INFO) 17 | # 5. 定义handler的输出格式 18 | formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') 19 | fh_stream.setFormatter(formatter) 20 | fh_file.setFormatter(formatter) 21 | # 6. 添加handler 22 | logger.addHandler(fh_stream) 23 | logger.addHandler(fh_file) 24 | 25 | # 7. 运行 26 | logger.info('this is a info') 27 | logger.debug('this is a debug') 28 | 29 | # 2019-11-20 23:29:13,977 log_file_demo INFO this is a info 30 | # 因为debug的级别小于info,所以不输出debug 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_login.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 1. 获取测试用例的列表 4 | # 获取testlogin.yml文件路径 5 | # 使用工具类来读取多个文档的内容 6 | # 2. 参数化执行测试用例 7 | 8 | import os 9 | import sys 10 | sys.path.append("../") 11 | import pytest 12 | from config import Conf 13 | from config.Conf import ConfigYaml 14 | from utils.YamlUtil import YamlReader 15 | from utils.RequestsUtil import Request 16 | 17 | test_file = os.path.join(Conf.get_data_path(), 'testlogin.yml') 18 | print(test_file) 19 | 20 | data_list = YamlReader(test_file).data_all() 21 | print(data_list) 22 | 23 | @pytest.mark.parametrize("login", data_list) 24 | def test_yaml(login): 25 | """ 26 | 执行测试用例 27 | """ 28 | uri = login['url'] 29 | print(uri) 30 | data = login['data'] 31 | print(data) 32 | request = Request(ConfigYaml().get_config_url()) 33 | res = request.post(uri, json=data) 34 | print(res) 35 | 36 | 37 | if __name__ == "__main__": 38 | pytest.main(['test_login.py']) 39 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_allure/allure_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import pytest 4 | import allure 5 | 6 | @allure.feature('接口测试, 这是一个一级标签') 7 | class TestAllure(): 8 | # 定义测试方法 9 | @allure.title('测试用例标题1') 10 | @allure.description('执行测试用例1的结果是:test_1') 11 | @allure.stroy("这是一个二级标签:test1") 12 | @allure.severity(allure.severity.CRITICAL) 13 | def test_1(self): 14 | print("test_1") 15 | 16 | @allure.title('测试用例标题2') 17 | @allure.description('执行测试用例2的结果是:test_2') 18 | @allure.stroy("这是一个二级标签:test1") 19 | @allure.severity(allure.severity.BLOCKER) 20 | def test_2(self): 21 | print("test_2") 22 | 23 | @allure.title('测试用例标题3') 24 | @allure.description('执行测试用例3的结果是:test_3') 25 | @allure.stroy("这是一个二级标签:test3") 26 | def test_3(self): 27 | print("test_3") 28 | 29 | @pytest.mark.parametrize("case",['case1', 'case2', 'case3']) 30 | def test_4(self, case): 31 | print(f"test4: {case}") 32 | allure.dynamic.title(case) 33 | 34 | 35 | if __name__ == "__main__": 36 | pytest.main(['allure_demo.py']) 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zhou_chengfei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /InterAutoTest_W/common/ExcelData.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 1. 使用excel工具类, 获取结果list 4 | # 2. 列"是否运行内容", y 5 | # 3. 保存要执行结果, 放到新的列表中 6 | import sys 7 | sys.path.append('../') 8 | from utils.ExcelUtil import ExcelReader 9 | from common.ExcelConfig import DataConfig 10 | 11 | class Data(): 12 | def __init__(self, excel_file, sheet_by): 13 | self.reader = ExcelReader(excel_file, sheet_by) 14 | 15 | def get_run_data(self): 16 | """ 17 | 根据"是否运行"列,获取执行测试用例 18 | """ 19 | run_list = [] 20 | for line in self.reader.data(): 21 | if str(line[DataConfig().is_run]).lower() == 'y': 22 | run_list.append(line) 23 | return run_list 24 | 25 | def get_case_list(self): 26 | """ 27 | 获取全部的测试用例 28 | """ 29 | run_list = [line for line in self.reader.data()] 30 | return run_list 31 | 32 | def get_case_pre(self, pre): 33 | """ 34 | 根据前置条件,从全部测试用例中返回前置用例 35 | """ 36 | # 获取前部测试用例 37 | # 判断需前置执行的用例 38 | run_list = self.get_case_list() 39 | for line in run_list: 40 | if pre in dict(line).values(): 41 | return line 42 | return None 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/AssertUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | 1. 定义封装类; 4 | 2. 初始化数据,日志 5 | 3. code相等 6 | 4. body相等 7 | 5.body包含 8 | """ 9 | import sys 10 | sys.path.append('../') 11 | import json 12 | from utils.LogUtil import my_log 13 | 14 | class AssertUtil(): 15 | 16 | def __init__(self): 17 | self.log = my_log('AssertUtil') 18 | 19 | def assert_code(self, code, expected_code): 20 | """ 21 | 验证返回状态码 22 | """ 23 | try: 24 | assert int(code) == int(expected_code) 25 | return True 26 | except Exception as e: 27 | self.log.error(f'code error, code is {code}, expected_code is {expected_code}') 28 | raise 29 | 30 | def assert_body(self, body, expected_body): 31 | """ 32 | 验证返回结果内容相等 33 | """ 34 | try: 35 | assert body == expected_body 36 | return True 37 | except Exception as e: 38 | self.log.error(f'body error, body is {body}, expected_body is {expected_body}') 39 | raise 40 | 41 | def assert_in_body(self, body, expected_body): 42 | """ 43 | 验证返回结果是否包含期望的结果 44 | """ 45 | try: 46 | body = json.dumps(body) 47 | assert expected_body in body 48 | return True 49 | except Exception as e: 50 | self.log.error(f'body error, body not in expected_body, body is {body}, expected_body is {expected_body}') 51 | raise -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/pytest_func.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | """ 4 | 1.定义类; 5 | 2.创建测试方法test开头 6 | 3.创建setup, teardown 7 | 4.运行查看结果 8 | """ 9 | import pytest 10 | 11 | class TestFcun(): 12 | def test_a(self): 13 | print('test_a') 14 | 15 | def test_b(self): 16 | print('test_b') 17 | 18 | def setup(self): 19 | print('------setup------') 20 | 21 | def teardown(self): 22 | print('------teardown------') 23 | 24 | if __name__ == "__main__": 25 | pytest.main(['-s', 'pytest_func.py']) 26 | 27 | """ 28 | PS E:\学习\测试\InterAutoTest_W\testcase\t_pytest> python pytest_func.py 29 | =========================================================================== test session starts =========================================================================== 30 | platform win32 -- Python 3.7.3, pytest-4.3.1, py-1.8.0, pluggy-0.9.0 31 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 32 | plugins: remotedata-0.3.1, openfiles-0.3.2, doctestplus-0.3.0, arraydiff-0.3 33 | collected 2 items 34 | 35 | pytest_func.py ------setup------ 36 | test_a 37 | .------teardown------ 38 | ------setup------ 39 | test_b 40 | .------teardown------ 41 | 42 | 43 | ======================================================================== 2 passed in 0.08 seconds ========================================================================= 44 | 45 | 46 | """ 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/ExcelUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 目的: 参数化, pytest list 4 | 5 | # 1. 验证文件是否存在,存在读取,不存在错报 6 | # 2. 读取sheet方式, 名称,索引 7 | # 3. 读取sheet内容 8 | # 返回list, 字典 9 | # 格式: [{'a':"a1",'b':" b1"}, {'a':"a2",'b':"b2"}] 10 | # 4. 结果返回 11 | 12 | import os 13 | import xlrd 14 | 15 | 16 | # 自定义异常 17 | class SheetTypeError(object): 18 | pass 19 | 20 | class ExcelReader(): 21 | def __init__(self, excel_file, sheet_by): 22 | if os.path.exists(excel_file): 23 | self.excel_file = excel_file 24 | self.sheet_by = sheet_by 25 | self.data_list = [] 26 | else: 27 | raise FileNotFoundError("文件不存在") 28 | 29 | def data(self): 30 | if self.data_list: 31 | return self.data_list 32 | 33 | workbook = xlrd.open_workbook(self.excel_file) 34 | 35 | if type(self.sheet_by) not in [str,int]: 36 | raise SheetTypeError("参数错误") 37 | elif type(self.sheet_by) == int: 38 | sheet = workbook.sheet_by_index(self.sheet_by) 39 | elif type(self.sheet_by) == str: 40 | sheet = workbook.sheet_by_name(self.sheet_by) 41 | 42 | # 获取首行信息 43 | title = sheet.row_values(0) 44 | for r in range(1, sheet.nrows): 45 | self.data_list.append(dict(zip(title,sheet.row_values(r)))) 46 | # print(self.data_list) 47 | return self.data_list 48 | 49 | if __name__ == "__main__": 50 | excel_reader = ExcelReader('../data/testdata.xlsx',"美多商城接口测试") 51 | print(excel_reader.data()) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/RequestsUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 1.创建封装方法 4 | # 2.发送requests请求 5 | # 3.获取结果相应内容 6 | # 4.内容存储到字典 7 | # 5.字典返回 8 | 9 | import requests 10 | from config.Conf import ConfigYaml 11 | 12 | def r_get(url, json=None, headers=None): 13 | r = requests.get(url,json=json, headers=headers) 14 | 15 | code = r.status_code 16 | try: 17 | body = r.json() 18 | except: 19 | body = r.text 20 | 21 | res = { 22 | 'code':code, 23 | 'body':body 24 | } 25 | 26 | return res 27 | 28 | 29 | def r_post(url, json=None, headers=None): 30 | r = requests.post(url,json=json, headers=headers) 31 | 32 | code = r.status_code 33 | try: 34 | body = r.json() 35 | except: 36 | body = r.text 37 | 38 | res = { 39 | 'code':code, 40 | 'body':body 41 | } 42 | 43 | return res 44 | 45 | # 重构 46 | class Request(): 47 | def __init__(self,url=ConfigYaml().get_config_url() ): 48 | self.url = url 49 | 50 | # 定义公共方法: 51 | def request_api(self, uri, method='get', data=None,json=None, headers=None,cookies=None): 52 | if method == 'get': 53 | r = requests.get(self.url+uri, data=data, json=json, headers=headers, cookies=cookies) 54 | elif method == 'post': 55 | r = requests.post(self.url+uri, data=data, json=json, headers=headers, cookies=cookies) 56 | 57 | code = r.status_code 58 | try: 59 | body = r.json() 60 | except: 61 | body = r.text 62 | res = { 63 | 'code':code, 64 | 'body':body 65 | } 66 | return res 67 | 68 | # 重构get方法 69 | def get(self, url,**kwargs): 70 | return self.request_api(url,**kwargs) 71 | 72 | def post(self, url,**kwargs): 73 | return self.request_api(url,method='post',**kwargs) 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/LogUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 封装工具类 4 | # 1.创建类 5 | # 2.定义参数 6 | # 输出文件名称,Loggername,日志级别 7 | # 3.编写输出到控制台或文件 8 | import sys 9 | sys.path.append('../') 10 | 11 | import logging 12 | import datetime, os 13 | from config import Conf 14 | from config.Conf import ConfigYaml 15 | 16 | log_l = { 17 | "info": logging.INFO, 18 | "debug": logging.DEBUG, 19 | "warning": logging.WARNING, 20 | "error": logging.ERROR 21 | } 22 | 23 | class Logger(): 24 | def __init__(self, log_file, log_name, log_level): 25 | self.log_file = log_file 26 | self.log_name = log_name 27 | self.log_level = log_level 28 | 29 | # 设置log名称 30 | self.logger = logging.getLogger(self.log_name) 31 | # 设置log级别 32 | self.logger.setLevel(log_l[self.log_level]) 33 | # 判断handler是否存在 34 | if not self.logger.handlers: 35 | # 输出到控制台 36 | fh_stream = logging.StreamHandler() 37 | fh_stream.setLevel(log_l[self.log_level]) 38 | formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') 39 | fh_stream.setFormatter(formatter) 40 | # 输出到文件 41 | fh_file = logging.FileHandler(self.log_file) 42 | fh_file.setLevel(log_l[self.log_level]) 43 | fh_file.setFormatter(formatter) 44 | # 添加到handler 45 | self.logger.addHandler(fh_stream) 46 | self.logger.addHandler(fh_file) 47 | 48 | 49 | # 1.初始化参数数据 50 | # 日志文件名称 51 | log_path = Conf.get_log_path() 52 | current_time = datetime.datetime.now().strftime('%Y-%m-%d') 53 | log_extension = ConfigYaml().get_conf_log_extension() 54 | logfile = os.path.join(log_path,current_time+log_extension) 55 | # print(logfile) 56 | 57 | # 日志文件级别 58 | loglevel = ConfigYaml().get_conf_log() 59 | # print(loglevel) 60 | 61 | # 2. 对外方法: 初始化log工具类, 提供其他类使用 62 | def my_log(log_name = __file__): 63 | return Logger(log_file=logfile, log_name=log_name, log_level=loglevel).logger 64 | 65 | if __name__ == "__main__": 66 | my_log().debug("this is a debug") 67 | -------------------------------------------------------------------------------- /InterAutoTest_W/config/Conf.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import sys 4 | sys.path.append('../') 5 | from utils.YamlUtil import YamlReader 6 | # 1. 获取项目基本目录 7 | # 1.2 获取当前项目的绝对路径 8 | current = os.path.abspath(__file__) 9 | BASE_DIR = os.path.dirname(os.path.dirname(current)) 10 | # print(current, BASE_DIR) 11 | # 1.3 定义config目录的路径 12 | _config_path = BASE_DIR + os.sep + "config" 13 | _data_path = BASE_DIR + os.sep + "data" 14 | # 1.4 定义conf.yml的文件路径 15 | _config_file = _config_path + os.sep + 'conf.yml' 16 | 17 | # 定义logs文件路径 18 | _log_path = BASE_DIR + os.sep + "logs" 19 | 20 | # 定义db_conf.yml文件路径 21 | _db_config_file = _config_path + os.sep + 'db_conf.yml' 22 | 23 | # 配置report路径 24 | _report_path = BASE_DIR + os.sep + "report" 25 | 26 | def get_report_path(): 27 | return _ewport_path 28 | 29 | def get_config_path(): 30 | return _config_path 31 | 32 | def get_data_path(): 33 | return _data_path 34 | 35 | def get_config_file(): 36 | return _config_file 37 | 38 | def get_log_path(): 39 | """ 40 | 获取log文件路径 41 | """ 42 | return _log_path 43 | 44 | def get_db_conf_file(): 45 | """ 46 | 获取db_conf文件路径 47 | """ 48 | return _db_config_file 49 | 50 | 51 | # 2. 读取配置文件 52 | class ConfigYaml(): 53 | def __init__(self): 54 | self.config = YamlReader(get_config_file()).data() 55 | self.db_config = YamlReader(get_db_conf_file()).data() 56 | 57 | def get_excel_file(self): 58 | """ 59 | 获取excel测试用例文件名称 60 | """ 61 | return self.config['BASE']['test']['case_file'] 62 | 63 | 64 | def get_excel_sheet(self): 65 | """ 66 | 获取excel测试用例文件sheet名称 67 | """ 68 | return self.config['BASE']['test']['case_sheet'] 69 | 70 | # 获取需要的信息 71 | def get_config_url(self): 72 | return self.config['BASE']['test']['url'] 73 | 74 | def get_conf_log(self): 75 | """ 76 | 获取日志级别 77 | """ 78 | return self.config['BASE']['log_level'] 79 | 80 | def get_conf_log_extension(self): 81 | return self.config['BASE']['log_extension'] 82 | 83 | def get_db_conf_info(self, db_alias): 84 | """ 85 | 根据db_alias获取数据库相关参数 86 | """ 87 | return self.db_config[db_alias] 88 | 89 | if __name__ == "__main__": 90 | conf_read = ConfigYaml() 91 | print(conf_read) 92 | print(conf_read.get_conf_log()) 93 | print(conf_read.get_conf_log_extension()) 94 | print(conf_read.get_db_conf_info('db_1')) 95 | # print(conf_read.get_excel_file()) 96 | # print(conf_read.get_excel_sheet()) 97 | -------------------------------------------------------------------------------- /InterAutoTest_W/utils/MysqlUtil.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | 1. 创建封装类 4 | 2.初始化数据,连接数据库,光标对象 5 | 3. 创建查询,执行方法 6 | 4. 关闭对象 7 | """ 8 | import sys 9 | sys.path.append('../') 10 | import pymysql 11 | import functools 12 | 13 | from utils.LogUtil import my_log 14 | 15 | class Mysql(): 16 | def __init__(self,host,user,password,database,port=3306,charset='utf8mb4'): 17 | self.cnn = pymysql.connect( 18 | host=host, 19 | port=port, 20 | user=user, 21 | password=password, 22 | database=database, 23 | charset=charset 24 | ) 25 | self.cursor = self.cnn.cursor(cursor=pymysql.cursors.DictCursor) # 结果使用dict返回 26 | self.log = my_log() 27 | 28 | def __del__(self): 29 | try: 30 | self.cnn.close() 31 | except Exception as e: 32 | pass 33 | 34 | try: 35 | self.cursor.close() 36 | except Exception as e: 37 | pass 38 | 39 | def fetch_one(self, sql): 40 | """ 41 | 查询一个对象 42 | """ 43 | self.cursor.execute(sql) 44 | return self.cursor.fetchone() 45 | 46 | def fetch_all(self, sql): 47 | """ 48 | 查询全部对象 49 | """ 50 | self.cursor.execute(sql) 51 | return self.cursor.fetchall() 52 | 53 | def exec(self,sql): 54 | """ 55 | 执行操作 56 | """ 57 | try: 58 | self.cursor.execute(sql) 59 | self.cursor.commit() 60 | except Exception as e: 61 | self.cnn.rollback() 62 | self.log.error(e) 63 | return False 64 | return True 65 | 66 | if __name__ == "__main__": 67 | mysql = Mysql( 68 | host='211.103.136.242', 69 | port=7090, 70 | user='test', 71 | password='test123456', 72 | database='meiduo', 73 | charset='utf8', 74 | ) 75 | res = mysql.fetch_all('select * from tb_uders') 76 | print(res) 77 | 78 | 79 | """ 80 | 1. 创建db_conf.yml 81 | 2. 编写数据库基本信息 82 | 3. 重构Conf.py 83 | 4. 执行 84 | """ 85 | 86 | 87 | # cnn = pymysql.connect( 88 | # host='211.103.136.242', 89 | # port=7090, 90 | # user='test', 91 | # password='test123456', 92 | # database='meiduo', 93 | # charset='utf8', 94 | # ) 95 | 96 | # with cnn.cursor() as cursor: 97 | # sql_str = 'select * from tb_users' 98 | # cursor.execute(sql_str) 99 | # res = cursor.fetchall() 100 | # print(res) 101 | 102 | # cnn.close() 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /InterAutoTest_W/common/Base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 1.定义一个方法init_db 3 | # 2.初始化数据库信息, 通过配置文件来完成 4 | # 3.初始化mysql对象 5 | import sys 6 | sys.path.append('../') 7 | import json 8 | import re 9 | import subprocess 10 | from config.Conf import ConfigYaml 11 | from utils.MysqlUtil import Mysql 12 | from utils.AssertUtil import AssertUtil 13 | from utils.LogUtil import my_log 14 | 15 | 16 | p_data = r'\${(.*)}\$' 17 | log = my_log() 18 | report_path = "" 19 | def init_db(db_alias): 20 | db_init = ConfigYaml().get_db_conf_info(db_alias) 21 | host = db_init.get('host', '127.0.0.1') 22 | port = int(db_init.get('port', 3306)) 23 | user = db_init.get('user') 24 | database = db_init.get('database') 25 | password = db_init.get('password') 26 | charset = db_init.get('charset', 'utf8') 27 | 28 | conn = Mysql(host=host,port=port,user=user,password=password,database=database,charset=charset) 29 | return conn 30 | 31 | def assert_db(db_name, res_body, db_verify): 32 | """ 33 | 数据库验证 34 | """ 35 | assert_util = AssertUtil() 36 | sql = init_db(db_name) 37 | db_res = sql.fetch_one(db_verify) 38 | log.debug(f"数据库查询结果:{str(db_res)}") 39 | for db_k,db_v in db_res.items(): 40 | res_line = res_body[db_k] 41 | assert_util.assert_body(res_line, db_v) 42 | 43 | def json_parse(data): 44 | """ 45 | 格式化数据 46 | """ 47 | return json.loads(data) if data else data 48 | 49 | def res_find(data, pattern_data=p_data): 50 | """ 51 | 查询匹配 52 | """ 53 | pattern = re.compile(pattern_data) 54 | res = pattern.findall(data) 55 | return res 56 | 57 | 58 | def res_sub(data, replace, pattern_data=p_data): 59 | """ 60 | 请求参数替换 61 | :param data: 源字符串 62 | :param replace: 替换内容 63 | :param pattern_data: 匹配规则 64 | """ 65 | pattern = re.compile(pattern_data) 66 | res = pattern.findall(data) 67 | if res: 68 | return re.sub(pattern_data,replace, data) 69 | else: 70 | return res 71 | 72 | def params_find(headers, cookies): 73 | """ 74 | 验证请求中是否有${}$需要结果关联 75 | 76 | """ 77 | if "${" in headers: 78 | headers = res_find(headers) 79 | if "${" in cookies: 80 | cookies = res_find(cookies) 81 | 82 | return headers, cookies 83 | 84 | def allure_report(report_path): 85 | allure_cmd = f"allure generate {report_path}/result -o {report_path}/html --clean" 86 | log.info(f"报告地址:{report_path}") 87 | try: 88 | subprocess.call(allure_cmd, shell=True) 89 | except Exception as e: 90 | log.error(e) 91 | 92 | 93 | if __name__ == "__main__": 94 | # conn = init_db('db_1') 95 | # print(conn) 96 | 97 | print(res_find('{"Authorization": "JWT ${token}$"}', r'\${(.*)}\$')) 98 | print(res_sub('{"Authorization": "JWT ${token}$"}', '123')) 99 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_mall.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import sys 3 | 4 | sys.path.append('../') 5 | import pytest 6 | import requests 7 | from utils.RequestsUtil import Request, r_get,r_post 8 | from common.Base import init_db 9 | 10 | request = Request(url='http://211.103.136.242:8064') 11 | 12 | def test_login(): 13 | """ 14 | 登录 15 | """ 16 | conn = init_db('db_1') 17 | res_db = conn.fetch_one('select id from tb_user where username="python"') 18 | print(f"数据库查询数据:{res_db}") 19 | 20 | url = 'http://211.103.136.242:8064/authorizations/' 21 | data = {'username':'python', 'password':'12345678'} 22 | 23 | r = r_post(url, json=data) 24 | print(r) 25 | ''' 26 | { 27 | 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzM1NzE3ODAsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.CDnyS9thrPFk40Z7PX4vbBTJnzQT582xlsaFZbkRnMs', 28 | 'username': 'python', 29 | 'user_id': 1 30 | } 31 | ''' 32 | 33 | def info(): 34 | url = 'http://211.103.136.242:8064/user/' 35 | token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzM1NzE3ODAsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.CDnyS9thrPFk40Z7PX4vbBTJnzQT582xlsaFZbkRnMs' 36 | headers = { 37 | 'Authorization':'JWT ' + token 38 | } 39 | 40 | r = r_get(url, headers=headers) 41 | 42 | print(r) 43 | 44 | def goods_list(): 45 | url = 'http://211.103.136.242:8064/categories/115/skus/' 46 | data = { 47 | 'page':"1", 48 | 'page_size':"10", 49 | 'ordering':"create_time" 50 | } 51 | 52 | r = r_get(url,json=data) 53 | print(r) 54 | 55 | def cart(): 56 | url = 'http://211.103.136.242:8064/cart/' 57 | data = { 58 | 'sku_id':"3", 59 | 'count':"1", 60 | 'selected':True 61 | } 62 | token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzM1NzE3ODAsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.CDnyS9thrPFk40Z7PX4vbBTJnzQT582xlsaFZbkRnMs' 63 | headers = { 64 | 'Authorization':'JWT ' + token 65 | } 66 | 67 | r = r_post(url, json=data, headers=headers) 68 | 69 | print(r) 70 | 71 | 72 | def order(): 73 | url = 'http://211.103.136.242:8064/orders/' 74 | data = { 75 | 'address':"1", 76 | 'pay_method':"1" 77 | } 78 | token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzM1NzE3ODAsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.CDnyS9thrPFk40Z7PX4vbBTJnzQT582xlsaFZbkRnMs' 79 | headers = { 80 | 'Authorization':'JWT ' + token 81 | } 82 | r = r_post(url, json=data, headers=headers) 83 | 84 | print(r) 85 | 86 | if __name__ == "__main__": 87 | # login() 88 | # info() 89 | # goods_list() 90 | # cart() 91 | # order() 92 | pytest.main(['-s']) 93 | 94 | 95 | 96 | 97 | """ 98 | 1. 根据默认运行原则,调整py文件命名,函数命名 99 | 2. pytest.main()运行,或者命令行直接运行 100 | """ 101 | 102 | 103 | -------------------------------------------------------------------------------- /InterAutoTest_W/report/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 12px; 4 | /* do not increase min-width as some may use split screens */ 5 | min-width: 800px; 6 | color: #999; 7 | } 8 | 9 | h1 { 10 | font-size: 24px; 11 | color: black; 12 | } 13 | 14 | h2 { 15 | font-size: 16px; 16 | color: black; 17 | } 18 | 19 | p { 20 | color: black; 21 | } 22 | 23 | a { 24 | color: #999; 25 | } 26 | 27 | table { 28 | border-collapse: collapse; 29 | } 30 | 31 | /****************************** 32 | * SUMMARY INFORMATION 33 | ******************************/ 34 | 35 | #environment td { 36 | padding: 5px; 37 | border: 1px solid #E6E6E6; 38 | } 39 | 40 | #environment tr:nth-child(odd) { 41 | background-color: #f6f6f6; 42 | } 43 | 44 | /****************************** 45 | * TEST RESULT COLORS 46 | ******************************/ 47 | span.passed, .passed .col-result { 48 | color: green; 49 | } 50 | span.skipped, span.xfailed, span.rerun, .skipped .col-result, .xfailed .col-result, .rerun .col-result { 51 | color: orange; 52 | } 53 | span.error, span.failed, span.xpassed, .error .col-result, .failed .col-result, .xpassed .col-result { 54 | color: red; 55 | } 56 | 57 | 58 | /****************************** 59 | * RESULTS TABLE 60 | * 61 | * 1. Table Layout 62 | * 2. Extra 63 | * 3. Sorting items 64 | * 65 | ******************************/ 66 | 67 | /*------------------ 68 | * 1. Table Layout 69 | *------------------*/ 70 | 71 | #results-table { 72 | border: 1px solid #e6e6e6; 73 | color: #999; 74 | font-size: 12px; 75 | width: 100% 76 | } 77 | 78 | #results-table th, #results-table td { 79 | padding: 5px; 80 | border: 1px solid #E6E6E6; 81 | text-align: left 82 | } 83 | #results-table th { 84 | font-weight: bold 85 | } 86 | 87 | /*------------------ 88 | * 2. Extra 89 | *------------------*/ 90 | 91 | .log:only-child { 92 | height: inherit 93 | } 94 | .log { 95 | background-color: #e6e6e6; 96 | border: 1px solid #e6e6e6; 97 | color: black; 98 | display: block; 99 | font-family: "Courier New", Courier, monospace; 100 | height: 230px; 101 | overflow-y: scroll; 102 | padding: 5px; 103 | white-space: pre-wrap 104 | } 105 | div.image { 106 | border: 1px solid #e6e6e6; 107 | float: right; 108 | height: 240px; 109 | margin-left: 5px; 110 | overflow: hidden; 111 | width: 320px 112 | } 113 | div.image img { 114 | width: 320px 115 | } 116 | .collapsed { 117 | display: none; 118 | } 119 | .expander::after { 120 | content: " (show details)"; 121 | color: #BBB; 122 | font-style: italic; 123 | cursor: pointer; 124 | } 125 | .collapser::after { 126 | content: " (hide details)"; 127 | color: #BBB; 128 | font-style: italic; 129 | cursor: pointer; 130 | } 131 | 132 | /*------------------ 133 | * 3. Sorting items 134 | *------------------*/ 135 | .sortable { 136 | cursor: pointer; 137 | } 138 | 139 | .sort-icon { 140 | font-size: 0px; 141 | float: left; 142 | margin-right: 5px; 143 | margin-top: 5px; 144 | /*triangle*/ 145 | width: 0; 146 | height: 0; 147 | border-left: 8px solid transparent; 148 | border-right: 8px solid transparent; 149 | } 150 | 151 | .inactive .sort-icon { 152 | /*finish triangle*/ 153 | border-top: 8px solid #E6E6E6; 154 | } 155 | 156 | .asc.active .sort-icon { 157 | /*finish triangle*/ 158 | border-bottom: 8px solid #999; 159 | } 160 | 161 | .desc.active .sort-icon { 162 | /*finish triangle*/ 163 | border-top: 8px solid #999; 164 | } 165 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/report/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 12px; 4 | /* do not increase min-width as some may use split screens */ 5 | min-width: 800px; 6 | color: #999; 7 | } 8 | 9 | h1 { 10 | font-size: 24px; 11 | color: black; 12 | } 13 | 14 | h2 { 15 | font-size: 16px; 16 | color: black; 17 | } 18 | 19 | p { 20 | color: black; 21 | } 22 | 23 | a { 24 | color: #999; 25 | } 26 | 27 | table { 28 | border-collapse: collapse; 29 | } 30 | 31 | /****************************** 32 | * SUMMARY INFORMATION 33 | ******************************/ 34 | 35 | #environment td { 36 | padding: 5px; 37 | border: 1px solid #E6E6E6; 38 | } 39 | 40 | #environment tr:nth-child(odd) { 41 | background-color: #f6f6f6; 42 | } 43 | 44 | /****************************** 45 | * TEST RESULT COLORS 46 | ******************************/ 47 | span.passed, .passed .col-result { 48 | color: green; 49 | } 50 | span.skipped, span.xfailed, span.rerun, .skipped .col-result, .xfailed .col-result, .rerun .col-result { 51 | color: orange; 52 | } 53 | span.error, span.failed, span.xpassed, .error .col-result, .failed .col-result, .xpassed .col-result { 54 | color: red; 55 | } 56 | 57 | 58 | /****************************** 59 | * RESULTS TABLE 60 | * 61 | * 1. Table Layout 62 | * 2. Extra 63 | * 3. Sorting items 64 | * 65 | ******************************/ 66 | 67 | /*------------------ 68 | * 1. Table Layout 69 | *------------------*/ 70 | 71 | #results-table { 72 | border: 1px solid #e6e6e6; 73 | color: #999; 74 | font-size: 12px; 75 | width: 100% 76 | } 77 | 78 | #results-table th, #results-table td { 79 | padding: 5px; 80 | border: 1px solid #E6E6E6; 81 | text-align: left 82 | } 83 | #results-table th { 84 | font-weight: bold 85 | } 86 | 87 | /*------------------ 88 | * 2. Extra 89 | *------------------*/ 90 | 91 | .log:only-child { 92 | height: inherit 93 | } 94 | .log { 95 | background-color: #e6e6e6; 96 | border: 1px solid #e6e6e6; 97 | color: black; 98 | display: block; 99 | font-family: "Courier New", Courier, monospace; 100 | height: 230px; 101 | overflow-y: scroll; 102 | padding: 5px; 103 | white-space: pre-wrap 104 | } 105 | div.image { 106 | border: 1px solid #e6e6e6; 107 | float: right; 108 | height: 240px; 109 | margin-left: 5px; 110 | overflow: hidden; 111 | width: 320px 112 | } 113 | div.image img { 114 | width: 320px 115 | } 116 | .collapsed { 117 | display: none; 118 | } 119 | .expander::after { 120 | content: " (show details)"; 121 | color: #BBB; 122 | font-style: italic; 123 | cursor: pointer; 124 | } 125 | .collapser::after { 126 | content: " (hide details)"; 127 | color: #BBB; 128 | font-style: italic; 129 | cursor: pointer; 130 | } 131 | 132 | /*------------------ 133 | * 3. Sorting items 134 | *------------------*/ 135 | .sortable { 136 | cursor: pointer; 137 | } 138 | 139 | .sort-icon { 140 | font-size: 0px; 141 | float: left; 142 | margin-right: 5px; 143 | margin-top: 5px; 144 | /*triangle*/ 145 | width: 0; 146 | height: 0; 147 | border-left: 8px solid transparent; 148 | border-right: 8px solid transparent; 149 | } 150 | 151 | .inactive .sort-icon { 152 | /*finish triangle*/ 153 | border-top: 8px solid #E6E6E6; 154 | } 155 | 156 | .asc.active .sort-icon { 157 | /*finish triangle*/ 158 | border-bottom: 8px solid #999; 159 | } 160 | 161 | .desc.active .sort-icon { 162 | /*finish triangle*/ 163 | border-top: 8px solid #999; 164 | } 165 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/test_excel_case.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 1. 初始化信息 4 | # 1) 初始化测试用例文件 5 | # 2) 测试用例sheet名称 6 | # 3) 获取运行测试用例列表 7 | # 4) 日志 8 | # 2. 测试用例方法,参数化运行 9 | 10 | import os 11 | import sys 12 | sys.path.append('../') 13 | import pytest 14 | import json 15 | import re 16 | import allure 17 | 18 | from config import Conf 19 | from config.Conf import ConfigYaml 20 | from common.ExcelData import Data 21 | from utils.LogUtil import my_log 22 | from common import ExcelConfig 23 | from common import Base 24 | from utils.RequestsUtil import Request 25 | from utils.AssertUtil import AssertUtil 26 | 27 | case_file = os.path.join(Conf.get_data_path, ConfigYaml().get_excel_file()) 28 | sheet_name = ConfigYaml().get_excel_sheet() 29 | data_init = Data(case_file, sheet_name) 30 | run_list = data_init.get_run_data() 31 | 32 | log = my_log() 33 | 34 | # 初始化dataconfig 35 | data_key = ExcelConfig.DataConfig 36 | 37 | # 一个用例运行 38 | # 1)初始化信息, url, data 39 | # 2) 接口请求 40 | class TestExcel(): 41 | # 1. 增加pytest 42 | # 2. 修改方法参数 43 | # 3. 重构函数内容 44 | # 4. pytest.main 45 | 46 | def run_pre(self, pre_case): 47 | url = pre_case[data_key.url] 48 | method = pre_case[data_key.method] 49 | params = pre_case[data_key.params] 50 | headers = pre_case[data_key.headers] 51 | cookies = pre_case[data_key.cookies] 52 | 53 | # 增加headers 54 | header = Base.json_parse(headers) 55 | # 增加cookies 56 | cookie = Base.json_parse(cookies) 57 | res = self.run_api(url, params, method, header, cookie) 58 | print(f"执行前置用例: {res}") 59 | return res 60 | 61 | 62 | def run_api(self,url, params, method ='get', header=None, cookie=None): 63 | """ 64 | 发送请求 65 | """ 66 | request = Request(ConfigYaml().get_config_url()) 67 | if not len(str(params).strip()): 68 | return params 69 | params = json.loads(params) 70 | if str(method).lower() == "get": 71 | res = request.get(url,json=params,headers=header,cookies=cookie) 72 | log.debug('get请求') 73 | elif str(method).lower() == "post": 74 | res = request.post(url, json=params,headers=header,cookies=cookie) 75 | log.debug('post请求') 76 | else: 77 | log.error(f"错误的请求方法:{method}") 78 | return res 79 | 80 | 81 | @pytest.mark.parametrize('case', run_list) 82 | def test_run(self,case): 83 | # data_key = ExcelConfig.DataConfig 84 | url = case[data_key.url] 85 | case_id = case[data_key.case_id] 86 | case_model = case[data_key.case_model] 87 | case_name = case[data_key.case_name] 88 | pre_exec = case[data_key.pre_exec] 89 | method = case[data_key.method] 90 | params_type = case[data_key.params_type] 91 | params = case[data_key.params] 92 | expect_result = case[data_key.expect_result] 93 | actual_result = case[data_key.actual_result] 94 | beizhu = case[data_key.beizhu] 95 | headers = case[data_key.headers] 96 | cookies = case[data_key.cookies] 97 | code = case[data_key.code] 98 | db_verify = case[data_key.db_verify] 99 | 100 | if pre_exec: 101 | # 前置用例 102 | pre_case = data_init.get_case_pre(pre_exec) 103 | # print(f"前置条件信息为:{pre_case}") 104 | pre_res = self.run_pre(pre_case) 105 | headers, cookies = self.get_correlation(headers, cookies,pre_res) 106 | 107 | # 增加headers 108 | header = Base.json_parse(headers) 109 | # 增加cookies 110 | cookie = Base.json_parse(cookies) 111 | request = Request(ConfigYaml().get_config_url()) 112 | # params转义json 113 | # 验证params有没有内容 114 | res = self.run_api(url, params, method, header, cookie) 115 | print(f"执行测试用例:{res}") 116 | 117 | # allure 118 | allure.dynamic.feature(sheet_name) 119 | allure.dynamic.story(case_model) 120 | allure.dynamic.title(case_id + case_name) 121 | desc = f"url:{url}
请求方法:{method}
期望结果:{expect_result}
实际结果:{res}" 122 | allure.dynamic.description(desc) 123 | 124 | 125 | # 断言验证 126 | assert_util = AssertUtil() 127 | assert_util.assert_code(int(res['code']), code) 128 | assert_util.assert_in_body(str(res['body']), str(expect_result)) 129 | 130 | 131 | # 数据库验证 132 | # sql = Base.init_db('db_1') 133 | # db_res = sql.fetch_one(db_verify) 134 | # log.debug(f"数据库查询结果:{str(db_res)}") 135 | # for db_k,db_v in db_res.items(): 136 | # res_line = res['body'][db_k] 137 | # assert_util.assert_body(res_line, db_v) 138 | Base.assert_db(db_name ='db_1', res_body=res['body'], db_verify=db_verify ) 139 | 140 | def get_correlation(self, headers, cookies, pre_res): 141 | """ 142 | 关联 143 | """ 144 | # 验证是否有关联 145 | headers_para, cookies_para= Base.params_find(headers, cookies) 146 | # 有关联执行前置用例,获取结果 147 | if len(headers_para): 148 | headers_data = pre_res['body'][headers_para[0]] 149 | # 结果替换 150 | headers = Base.res_sub(headers, headers_data) 151 | 152 | if len(cookies_para): 153 | cookies_data = pre_res['body'][cookies_para[0]] 154 | # 结果替换 155 | cookies = Base.res_sub(headers, cookies_data) 156 | 157 | return headers, cookies 158 | 159 | if __name__ == "__main__": 160 | report_path = Conf._report_path() 161 | pytest.main(['-s', 'test_excel_case.py', '--alluredir', report_path+'/reslut']) 162 | Base.allure_report(report_path) 163 | # TestExcel().test_run() 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /InterAutoTest_W/testcase/t_pytest/report/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Report 6 | 7 | 8 | 243 |

report.html

244 |

Report generated on 26-Nov-2019 at 18:15:56 by pytest-html v2.0.1

245 |

Environment

246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 |
JAVA_HOMEC:\Develop\java\JDK
Packages{'pytest': '5.3.0', 'py': '1.8.0', 'pluggy': '0.13.1'}
PlatformWindows-10-10.0.18362-SP0
Plugins{'arraydiff': '0.3', 'doctestplus': '0.3.0', 'html': '2.0.1', 'metadata': '1.8.0', 'openfiles': '0.3.2', 'remotedata': '0.3.1', 'rerunfailures': '8.0'}
Python3.7.3
262 |

Summary

263 |

2 tests ran in 0.05 seconds.

264 | 2 passed, 0 skipped, 0 failed, 0 errors, 0 expected failures, 0 unexpected passes, 0 rerun 265 |

Results

266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 |
ResultTestDurationLinks
Passedtestcase/t_pytest/pytest_more.py::TestClass::test_a[xiaoming-12345]0.00
283 |
No log output captured.
Passedtestcase/t_pytest/pytest_more.py::TestClass::test_a[xiaohong-56789]0.00
292 |
No log output captured.
-------------------------------------------------------------------------------- /InterAutoTest_W/report/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Report 6 | 7 | 8 | 243 |

report.html

244 |

Report generated on 28-Nov-2019 at 00:04:49 by pytest-html v2.0.1

245 |

Environment

246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 |
JAVA_HOMEC:\Develop\java\JDK
Packages{'pytest': '5.3.1', 'py': '1.8.0', 'pluggy': '0.13.1'}
PlatformWindows-10-10.0.18362-SP0
Plugins{'html': '2.0.1', 'metadata': '1.8.0', 'rerunfailures': '8.0'}
Python3.8.0
262 |

Summary

263 |

0 tests ran in 0.61 seconds.

264 | 0 passed, 0 skipped, 0 failed, 2 errors, 0 expected failures, 0 unexpected passes, 0 rerun 265 |

Results

266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 |
ResultTestDurationLinks
Errortestcase/test_excel_case.py::collect0.00
283 |
testcase\test_excel_case.py:24: in <module>
run_list = Data(case_file, sheet_name).get_run_data()
common\ExcelData.py:13: in __init__
self.reader = ExcelReader(excel_file, sheet_by)
utils\ExcelUtil.py:27: in __init__
raise FileNotFoundError("文件不存在")
E FileNotFoundError: 文件不存在
Errortestcase/test_mall.py::collect0.00
292 |
ImportError while importing test module 'E:\学习\测试\InterAutoTest_W\testcase\test_mall.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
testcase\test_mall.py:8: in <module>
from common.Base import init_db
common\Base.py:7: in <module>
from utils.MysqlUtil import Mysql
utils\MysqlUtil.py:11: in <module>
from utils.LogUItil import my_log
E ModuleNotFoundError: No module named 'utils.LogUItil'
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 自动化接口测试 3 | # 第一章 4 | ## 一、项目相关: 5 | ### 1.项目信息: 6 | #### 1.1 项目介绍: 7 | - 项目名称: 美多商城 8 | - 项目访问访问网站: http://211.103.136.242:8062/ 9 | ### 2.接口信息: 10 | #### 2.1 接口介绍: 11 | - 登录; 12 | - 个人信息; 13 | - 获取商品信息; 14 | - 购物车; 15 | - 订单; 16 | 17 | #### 2.2 登录接口文档: 18 | - 后端接口设计: 19 | - 请求方式: POST /authorizations/ 20 | - 请求参数: 21 | 22 | |参数名|类型|说明| 23 | |-|-|-| 24 | |username|str|用户名| 25 | |password|str|密码| 26 | 27 | 返回数据 28 | 29 | |返回值|类型|说明 | 30 | |-|-|-| 31 | |username|str|用户名| 32 | |user_id|int|用户id| 33 | |token|str|身份认证| 34 | #### 2.3 用户中心个人信息: 35 | 访问必须要求用户已通过认证(即登录之后) 36 | 认证: 37 | ```shell 38 | headers:{ 39 | 'Authorization': 'JWT '+ this.token 40 | } 41 | ``` 42 | - 后端接口设计: 43 | - 请求方法: GET /user/ 44 | - 返回数据: 45 | 46 | |返回值|类型|是否必须|说明| 47 | |-|-|-|-| 48 | |id|int|是|用户id| 49 | |username|str|是|用户名| 50 | |mobile|str|是|手机号| 51 | |email|str|是|email邮箱| 52 | |email_active|bool|是|邮箱是否通过验证| 53 | #### 2.4 获取商品列表数据: 54 | - 业务需求: 55 | 需要对商品数量进行分页支持,并且可以按照时间(默认)、价格、销量(人气)进行排序 56 | - 后端接口设计: 57 | - 请求方法: GET /categories/(?P\d+)/skus>page=xxx&page_size=xxx&ordering=xxx 58 | - 请求参数: 59 | 60 | |参数|类型|是否必须|说明| 61 | |-|-|-|-| 62 | |category_id|int|是|类别id(第三级类别)| 63 | |page|int|是|页数| 64 | |page_size|int|是|每页数量| 65 | |ordering|str|是|排序关键字(create_time, price, sales) 66 | 67 | 返回数据: 68 | 69 | |返回值|类型|是否必须|说明| 70 | |-|-|-|-| 71 | |id|int|是|商品sku编号| 72 | |name|str|是|商品名称| 73 | |price|decimal|是|单价| 74 | |default_image_url|str|是|默认图片| 75 | |comments|int|是|评论数量| 76 | 77 | #### 2.5 添加到购物车: 78 | - 后端接口: 79 | - 请求方法: POST /cart/ 80 | - 请求参数: 81 | 82 | |参数|类型|是否必须|说明| 83 | |-|-|-|-| 84 | |sku_id|int|是|商品sku_id| 85 | |count|int|是|数量| 86 | |selected|bool|否|是否勾选,默认勾选| 87 | 88 | 返回数据: 89 | 90 | |参数|类型| 是否必须|说明| 91 | |-|-|-|-| 92 | |sku_id|int|是|商品sku_id| 93 | |count|int|是|数量| 94 | |selected|bool|否|是否勾选,默认勾选| 95 | 96 | 访问此接口, 无论用户是否登录,前端请求都需要带请求头Authorization, 由后端判断是否登录. 97 | 98 | #### 2.6 保存订单: 99 | - 后端接口设计: 100 | - 请求方式: POST /orders/ 101 | - 请求参数: 102 | 103 | |参数|类型| 是否必须|说明| 104 | |-|-|-|-| 105 | |address|int|是|收货地址id| 106 | |pat_method|int|是|支付方式| 107 | 108 | 返回数据: 109 | 110 | |参数|类型| 是否必须|说明| 111 | |-|-|-|-| 112 | |order_id|char|是|订单编号| 113 | 114 | ## 二、接口测试框架: 115 | ### 1.介绍: 116 | #### 1.1 框架对比: 117 | - Unittest: 118 | - 不支持失败自动重新执行; 119 | - 参数化需依赖三方库; 120 | - HTMLTestRunner三方报告不够美观 121 | 122 | - pytest: 123 | - 兼容unittest 124 | - 支持失败自动重新执行; 125 | - 参数化使用自带装饰器; 126 | - 兼容主流allure框架,报告美观功能强大; 127 | 128 | ### 2.流程: 129 | #### 2.1 代码运行: 130 | ```flow 131 | op1=>operation: 主程序运行(pytest框架) 132 | op2=>operation: Excel用例(数据驱动) 133 | op3=>operation: 配置文件(Yaml) 134 | op4=>operation: Requests 135 | op5=>operation: 断言(结果断言、数据库断言) 136 | op6=>operation: 报告(Allure插件) 137 | op7=>operation: 邮件 138 | 139 | op1->op2->op3->op4->op5->op6->op7 140 | ``` 141 | #### 2.2 jenkins运行: 142 | ```flow 143 | op1=>operation: Jenkins/Docker 144 | op2=>operation: Job运行 145 | op3=>operation: git 146 | op4=>operation: Allure 147 | op5=>operation: 邮件 148 | 149 | op1->op2->op3->op4->op5 150 | ``` 151 | ![代码测试结构](./image/代码测试结构.jpg) 152 | 153 | # 第二章 接口自动化框架编写 154 | ## 一、项目及框架的搭建: 155 | ### 1.工具: 156 | #### 1.1 python: 157 | - 下载地址: https://www.python.org/download 158 | 159 | #### 1.2 pycharm: 160 | - 下载地址: https://www.jetbrains.com/pycharm 161 | 162 | #### 1.3 git: 163 | - 下载地址: https://git-scm.com/download 164 | 165 | ### 2.框架目录: 166 | #### 2.1 创建目录: 167 | ```shell 168 | InterAutoTest_W 169 | ``` 170 | ![代码测试结构](./image/代码测试结构.jpg) 171 | ### 3.配置(pycharm): 172 | - 配置python环境: 173 | ```shell 174 | Setting ->Project ->Project Interpreter 175 | ``` 176 | - 配置git: 177 | ```shell 178 | Setting ->Version Control ->Git 179 | ``` 180 | - 配置github: 181 | ```shell 182 | Setting -> Version Control ->Github 183 | ``` 184 | - 建立远程仓库并提交代码: 185 | ```shell 186 | Vcs ->import into version control ->Share Project on Github 187 | ``` 188 | ## 二、接口用例编写: 189 | ### 1.被测试的接口: 190 | - 登录; 191 | - 获取个人信息; 192 | - 获取商品信息; 193 | - 添加到购物车; 194 | - 保存订单; 195 | 196 | ### 2. 使用excel编写测试用例: 197 | #### 2.1 表结构: 198 | - 用例ID; 199 | - 模块; 200 | - 接口名称; 201 | - 请求URL; 202 | - 前置条件; 203 | - 请求类型; 204 | - 请求参数类型 205 | - 请求参数; 206 | - 预期结果; 207 | - 实际结果; 208 | - 备注; 209 | 210 | ## 三、Requests使用: 211 | ### 1. 介绍及使用: 212 | - 介绍:流行的接口http(s)请求工具, 功能强大,简单方便,容易上手; 213 | - 官网: http://cn.python-requests.org/zh_CN/latest/ 214 | 215 | ### 2. 用例代码编写: 216 | ```python 217 | # coding=utf-8 218 | import requests 219 | r = requests.get('http://www.baidu.com') 220 | print(r) # 221 | ``` 222 | - 请求返回介绍: 223 | 224 | |属性/方法|说明| 225 | |-|-| 226 | |r.status_code|响应状态码| 227 | |r.content|字节方式的响应体,会自动解码gzip和deflate压缩| 228 | |r.headers|以字典对象存储服务器响应头,若键不存在则返回None| 229 | |r.json()|Requests中内置的JSON| 230 | |r.url|获取url| 231 | |r.encoding|编码格式| 232 | |r.cookies|获取cookie| 233 | |r.raw|获取原始响应体| 234 | |r.text|字符串方式的响应体,会自动根据响应头部的字符编码进行编码| 235 | |r.raise_for_status()|失败请求(非200响应)抛出异常| 236 | 237 | ### 3. 方法封装: 238 | - 1.创建封装方法 239 | - 2.发送requests请求 240 | - 3.获取结果相应内容 241 | - 4.内容存储到字典 242 | - 5.字典返回 243 | utils/Request.py 244 | ```python 245 | import requests 246 | 247 | class Request(): 248 | def __init__(self,url): 249 | self.url = url 250 | 251 | # 定义公共方法: 252 | def request_api(self, uri, method='get', data=None,json=None, headers=None): 253 | if method == 'get': 254 | r = requests.get(self.url+uri, data=data, json=json, headers=headers) 255 | elif method == 'post': 256 | r = requests.post(self.url+uri, data=data, json=json, headers=headers) 257 | 258 | code = r.status_code 259 | try: 260 | body = r.json() 261 | except: 262 | body = r.text 263 | res = { 264 | 'code':code, 265 | 'body':body 266 | } 267 | return res 268 | 269 | # 重构get方法 270 | def get(self, uri,**kwargs): 271 | return self.request_api(uri,**kwargs) 272 | 273 | def post(self, uri,**kwargs): 274 | return self.request_api(uri,method='post',**kwargs) 275 | ``` 276 | 277 | ## 四、配置文件: 278 | 使用YAML语言编写配置文件 279 | ### 1. yaml的介绍与安装: 280 | #### 1.1 Yaml介绍: 281 | - yaml是一种所有编程语言可用的友好的数据序列化标准, 语法和其他高阶语言类似,并且可以简单表达字典、列表和其他基本数据类型的形态. 282 | - yaml格式作为文件的配置格式: 283 | - yaml支持注释; 284 | - 不必强求逗号、括号等符号; 285 | - 通过缩进来区分,视觉上清晰 286 | - yaml官网: https://yaml.org/ 287 | 288 | #### 1.2 Yaml安装: 289 | ```shell 290 | pip install PyYaml 291 | ``` 292 | #### 1.3 快速体验: 293 | - 字典:字典里的键值对用':'分隔; 294 | data.yml 295 | ```yaml 296 | name: "test_yaml" 297 | result: "success" 298 | ``` 299 | ```python 300 | import yaml 301 | 302 | with open('./data.yml', 'r') as f: 303 | r = yaml.safe_load(f) 304 | 305 | print(r) 306 | # {'name': 'test_yaml', 'result': 'success'} 307 | ``` 308 | 309 | ### 2. 基本操作: 310 | #### 2.1 字典: 311 | - 字典里的键值对用':'分隔; 312 | - 字典直接写key: value, 每个键值对占一行; 313 | - key: 后要跟空格 314 | #### 2.2 列表: 315 | - 一组按序列排列的值(简称"序列或列表"); 316 | - 数组前加有"-"符号,符号与值之间需要用空格分隔; 317 | ```yaml 318 | - 12 319 | - 32 320 | - 33 321 | # [12, 32, 33] 322 | ``` 323 | #### 2.3 相互嵌套: 324 | - 字典嵌套字典: 325 | ```yaml 326 | person1: 327 | name: xiaoming 328 | age: 18 329 | person2: 330 | name: xiaohong 331 | age: 16 332 | 333 | # person1': {'name': 'xiaoming', 'age': 18}, 'person2': {'name': 'xiaohong', 'age': 16}} 334 | ``` 335 | - 字典嵌套列表: 336 | ```yaml 337 | person: 338 | - "a" 339 | - "b" 340 | - c 341 | 342 | # {'person': ['a', 'b', 'c']} 343 | ``` 344 | 345 | - 列表嵌套列表: 346 | ```yaml 347 | - 348 | - 1 349 | - 2 350 | - 3 351 | - "b" 352 | - c 353 | - 354 | - 6 355 | - 7 356 | - 8 357 | 358 | # [[1, 2, 3], 'b', 'c', [6, 7, 8]] 359 | ``` 360 | - 列表嵌套字典: 361 | ```yaml 362 | - 363 | name: xiaoming 364 | age: 18 365 | - 2 366 | - 3 367 | - 368 | name: xiaohong 369 | age: 16 370 | 371 | # [{'name': 'xiaoming', 'age': 18}, 2, 3, {'name': 'xiaohong', 'age': 16}] 372 | ``` 373 | 374 | ### 3.读取文件: 375 | #### 3.1 单个文件: 376 | * 中文乱码: open指定encoding* 377 | 378 | data.yml 379 | ```yaml 380 | name: "test_yaml" 381 | result: "success" 382 | ``` 383 | ```python 384 | import yaml 385 | 386 | with open('./data.yml', 'r') as f: 387 | r = yaml.safe_load(f) 388 | 389 | print(r) 390 | # {'name': 'test_yaml', 'result': 'success'} 391 | ``` 392 | 393 | #### 3.2 多个文件: 394 | data.yml 395 | ```yaml 396 | --- 397 | name: "test_yaml" 398 | result: "success" 399 | 400 | --- 401 | # 用'---'分隔,说明是多个文档 402 | "用户名称1": "test123" 403 | "密码":"123456" 404 | ``` 405 | ```python 406 | import yaml 407 | 408 | with open('./data.yml', 'r') as f: 409 | r = yaml.safe_load_all(f) 410 | for i in r: 411 | print(r) 412 | # {'name': 'test_yaml', 'result': 'success'} 413 | # {"用户名称1": "test123","密码":"123456"} 414 | ``` 415 | 416 | ### 4.配置文件设置: 417 | - yaml封装: 418 | ./utils/YamlUtil.py 419 | ```python 420 | # coding=utf-8 421 | 422 | # 1. 创建类 423 | # 2. 初始化,文件是否存在 424 | # 3. yaml读取 425 | 426 | import os 427 | import yaml 428 | 429 | class YamlReader(): 430 | def __init__(self, yaml_p): 431 | if os.path.exists(yaml_p): 432 | self.yaml_p = yaml_p 433 | else: 434 | raise FileNotFoundError("文件不存在") 435 | self._data = None 436 | self._data_all = None 437 | 438 | def data(self): 439 | # 读取单个文档 440 | if not self._data: 441 | with open(self.yaml_p, 'r') as f: 442 | self._data = yaml.safe_load(f) 443 | return self._data 444 | 445 | def data_all(self): 446 | # 读取单个文档 447 | if not self._data: 448 | with open(self.yaml_p, 'r') as f: 449 | self._data_all = list(yaml.safe_load_all(f)) 450 | return self._data_all 451 | ``` 452 | - 配置文件conf.yaml 453 | ./config/conf.yml 454 | ```yaml 455 | BASE: 456 | test: 457 | url: "http://211.103.136.242:8064" 458 | 459 | ``` 460 | 461 | 462 | ./config/Conf.py 463 | ```python 464 | # coding=utf-8 465 | import os 466 | from utils.YamlUtil import YamlReader 467 | # 1. 获取项目基本目录 468 | # 1.2 获取当前项目的绝对路径 469 | current = os.path.abspath(__file__) 470 | BASE_DIR = os.path.dirname(os.path.dirname(current)) 471 | print(current, BASE_DIR) 472 | # 1.3 定义config目录的路径 473 | _config_path = BASE_DIR + os.sep + "config" 474 | 475 | def get_config_path(): 476 | return _config_path 477 | # 1.4 定义conf.yml的文件路径 478 | _config_file = _config_path + os.sep + 'conf.yml' 479 | def get_config_file(): 480 | return _config_file 481 | 482 | # 2. 读取配置文件 483 | class ConfigYaml(): 484 | def __init__(self): 485 | self.config = YamlReader(get_config_file()).data() 486 | 487 | # 获取需要的信息 488 | def get_config_url(self): 489 | return self.config['BASE']['test']['url'] 490 | 491 | if __name__ == "__main__": 492 | conf_read = ConfigYaml() 493 | print(conf_read) 494 | 495 | ``` 496 | - 基本目录配置: 497 | 498 | - 配置文件读取及使用: 499 | 500 | ## 五、日志文件: 501 | ### 1.介绍: 502 | #### 1.1 简介: 503 | logging模块是python内置的标准模块,主要用于输出运行日志, 可以设置输出日志等级、日志保存路径等. 504 | 505 | #### 1.2 快速使用: 506 | log_demo.py 507 | ```python 508 | # coding=utf-8 509 | # 1.导入logging包 510 | import logging 511 | # 2.设置配置信息 512 | logging.basicConfig(level=logging.INFO, format="%(asctime)s-%(name)s-%(levelname)s-%(message)s") 513 | # 3. 定义日志名称:get_logger 514 | logger = logging.getLogger('log_demo') 515 | # 4. info,debug 516 | logger.info('info') 517 | logger.debug('debug') 518 | logger.warning('warning') 519 | 520 | # 2019-11-20 23:06:09,957-log_demo-INFO-info 521 | # 2019-11-20 23:06:09,958-log_demo-WARNING-warning 522 | 523 | ``` 524 | 525 | ### 2.基本使用: 526 | #### 2.1 日志输出到控制台或文件: 527 | - 1. 设置logger名称 528 | - 2. 设置log级别 529 | - 3. 创建handler, 用于输出控制台或写入文件 530 | - 4. 设置日志级别 531 | - 5. 定义handler的输出格式 532 | - 6. 添加handler 533 | 534 | ```python 535 | # coding=utf-8 536 | import logging 537 | 538 | # 1. 设置logger名称 539 | logger = logging.getLogger('log_file_demo') 540 | # 2. 设置log级别 541 | logger.setLevel(logging.INFO) 542 | # 3. 创建handler, 用于输出控制台或写入文件 543 | # 输出到控制台 544 | fh_stream = logging.StreamHandler() 545 | # 写入文件 546 | fh_file = logging.FileHandler('./test.log') 547 | # 4. 设置日志级别 548 | fh_stream.setLevel(logging.INFO) 549 | 550 | fh_file.setLevel(logging.INFO) 551 | # 5. 定义handler的输出格式 552 | formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') 553 | fh_stream.setFormatter(formatter) 554 | 555 | fh_file.setFormatter(formatter) 556 | # 6. 添加handler 557 | logger.addHandler(fh_stream) 558 | 559 | logger.addHandler(fh_file) 560 | 561 | # 7. 运行 562 | logger.info('this is a info') 563 | logger.debug('this is a debug') 564 | 565 | # 2019-11-20 23:29:13,977 log_file_demo INFO this is a info 566 | # 因为debug的级别小于info,所以不输出debug 567 | 568 | ``` 569 | 570 | #### 2.2 log级别: 571 | 572 | #### 2.3 输出格式: 573 | Format格式说明: 574 | |格式|说明| 575 | |-|-| 576 | |%(levelno)s |打印日志级别的数值| 577 | |%(levelname)s |打印日志级别名称| 578 | |%(pathname)s |打印当前执行程序的路径,其实是sys.argv[0]| 579 | |%(filename)s |打印当前执行程序名| 580 | |%(funcName)s |打印日志的当前函数| 581 | |%(lineno)d |打印诶之的当前行号| 582 | |%(asctime)s |打印日志的时间| 583 | |%(thread)d |打印线程ID| 584 | |%(threadName)s |打印线程名称| 585 | |%(process)d |打印线程ID| 586 | |%(message)s |打印日志信息| 587 | 588 | ### 3.封装工具类: 589 | #### 3.1 封装Log工具类: 590 | utils/LogUtil.py 591 | ```python 592 | # coding=utf-8 593 | 594 | # 封装工具类 595 | # 1.创建类 596 | # 2.定义参数 597 | # 输出文件名称,Loggername,日志级别 598 | # 3.编写输出到控制台或文件 599 | 600 | import logging 601 | 602 | log_l = { 603 | "info": logging.INFO, 604 | "debug": logging.DEBUG, 605 | "warning": logging.WARNING, 606 | "error": logging.ERROR 607 | } 608 | 609 | class logger(): 610 | def __init__(self, log_file, log_name, log_level): 611 | self.log_file = log_file # 扩展名, 配置文件 612 | self.log_name = log_name # 613 | self.log_level = log_level 614 | 615 | # 设置log名称 616 | self.logger = logging.getLogger(self.log_nam) 617 | # 设置log级别 618 | self.logger.setLevel(log_l[self.log_level]) 619 | # 判断handler是否存在 620 | if not self.logger.handlers: 621 | # 输出到控制台 622 | fh_stream = logging.StreamHandler() 623 | fh_stream.setLevel(log_l[self.log_level]) 624 | formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') 625 | fh_stream.setFormatter(formatter) 626 | # 输出到文件 627 | fh_file = logging.FileHandler(self.log_file) 628 | fh_file.setLevel(log_l[self.log_level]) 629 | fh_file.setFormatter(formatter) 630 | # 添加到handler 631 | self.logger.addHandler(fh_stream) 632 | self.logger.addHandler(fh_file) 633 | ``` 634 | 635 | #### 3.2 重构配置文件: 636 | config/conf.yml 637 | ```yaml 638 | BASE: 639 | # log等级 640 | log_level: 'debug' 641 | # 扩展名 642 | log_extension: '.log' 643 | 644 | test: 645 | url: "http://211.103.136.242:8064" 646 | ``` 647 | #### 3.3 日志工具类应用 648 | config/Conf.py 649 | ```python 650 | # coding=utf-8 651 | import os 652 | from utils.YamlUtil import YamlReader 653 | # 1. 获取项目基本目录 654 | # 1.2 获取当前项目的绝对路径 655 | current = os.path.abspath(__file__) 656 | BASE_DIR = os.path.dirname(os.path.dirname(current)) 657 | print(current, BASE_DIR) 658 | # 1.3 定义config目录的路径 659 | _config_path = 660 | # 1.4 定义conf.yml的文件路径 661 | _config_file = _config_path + os.sep + 'conf.yml' 662 | 663 | # 定义logs文件路径 664 | _log_path = BASE_DIR + os.sep + "logs" 665 | 666 | def get_config_path(): 667 | return _config_path 668 | 669 | def get_config_file(): 670 | return _config_file 671 | 672 | def get_log_path(): 673 | """ 674 | 获取log文件路径 675 | """ 676 | return _log_path 677 | 678 | # 2. 读取配置文件 679 | class ConfigYaml(): 680 | def __init__(self): 681 | self.config = YamlReader(get_config_file()).data() 682 | 683 | # 获取需要的信息 684 | def get_config_url(self): 685 | return self.config['BASE']['test']['url'] 686 | 687 | def get_conf_log(self): 688 | """ 689 | 获取日志级别 690 | """ 691 | return self.config['BASE_DIR']['log_level'] 692 | 693 | def get_conf_log_extension(self): 694 | return self.config['BASE_DIR']['log_extension'] 695 | 696 | if __name__ == "__main__": 697 | conf_read = ConfigYaml() 698 | print(conf_read) 699 | print(conf_read.get_conf_log()) 700 | print(conf_read.get_conf_log_extension()) 701 | ``` 702 | 703 | utils/LogUtil.py 704 | ```python 705 | # 1.初始化参数数据 706 | # 日志文件名称 707 | log_path = Conf.get_log_path() 708 | current_time = datetime.datetime.now().strftime('%Y-%m-%d') 709 | log_extension = ConfigYaml().get_conf_log_extension() 710 | logfile = os.path.join(log_path,current_time+log_extension) 711 | print(logfile) 712 | 713 | # 日志文件级别 714 | loglevel = ConfigYaml().get_conf_log() 715 | print(loglevel) 716 | 717 | # 2. 对外方法: 初始化log工具类, 提供其他类使用 718 | def my_log(log_name = __file__): 719 | return Logger(log_file=logfile, log_name=log_name, log_level=loglevel) 720 | 721 | if __name__ == "__main__": 722 | my_log().debug("this is a debug") 723 | 724 | ``` 725 | 726 | ## 六、pytesy框架: 727 | ### 1.安装与入门: 728 | #### 1.1 介绍: 729 | - 简单灵活; 730 | - 容易上手; 731 | - 文档丰富; 732 | - 支持参数化 733 | ### 2.基础使用: 734 | ```python 735 | #coding=utf-8 736 | 737 | # 1. 创建简单的测试方法 738 | # 2. pytest运行 739 | # 2.1 idea中直接执行 740 | # 2.2 命令行执行 741 | import pytest 742 | 743 | # 创建普通的方法 744 | def func(x): 745 | return x+1 746 | 747 | # 创建pytest断言的方法 748 | def test_a(): 749 | print("---test_a---") 750 | assert func(3) == 5 # 断言失败 751 | 752 | def test_b(): 753 | print('---test_b---') 754 | assert func(3) == 4 # 断言成功 755 | 756 | # 代码直接执行 757 | if __name__ == "__main__": 758 | pytest.main(["pytest_demo.py"]) 759 | ``` 760 | 使用命令行直接执行测试脚本: 761 | ```shell 762 | pytest pytest_demo.py 763 | ``` 764 | 765 | #### 2.1 函数级别方法: 766 | - 运行于*测试*方法的始末; 767 | - 运行一次测试函数会运行一次setup和teardown; 768 | 769 | ```python 770 | #coding=utf-8 771 | 772 | """ 773 | 1.定义类; 774 | 2.创建测试方法test开头 775 | 3.创建setup, teardown 776 | 4.运行查看结果 777 | """ 778 | import pytest 779 | 780 | class TestFcun(): 781 | def test_a(self): 782 | print('test_a') 783 | 784 | def test_b(self): 785 | print('test_b') 786 | 787 | def setup(self): 788 | print('------setup------') 789 | 790 | def teardown(self): 791 | print('------teardown------') 792 | 793 | if __name__ == "__main__": 794 | pytest.main(['-s', 'pytest_func.py']) 795 | 796 | """ 797 | PS E:\学习\测试\InterAutoTest_W\testcase\t_pytest> python pytest_func.py 798 | ================================== test session starts ================================ 799 | platform win32 -- Python 3.7.3, pytest-4.3.1, py-1.8.0, pluggy-0.9.0 800 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 801 | plugins: remotedata-0.3.1, openfiles-0.3.2, doctestplus-0.3.0, arraydiff-0.3 802 | collected 2 items 803 | 804 | pytest_func.py ------setup------ 805 | test_a 806 | .------teardown------ 807 | ------setup------ 808 | test_b 809 | .------teardown------ 810 | 811 | 812 | ================= 2 passed in 0.08 seconds ================== 813 | """ 814 | ``` 815 | 816 | #### 2.2 类级别方法: 817 | - 运行于测试*类*的始末; 818 | - 一个测试内只运行一次setup_class和teardown_class,不关心测试类内有多少测试函数 819 | ```python 820 | #coding=utf-8 821 | 822 | """ 823 | 1.定义类; 824 | 2.创建测试方法test开头 825 | 3.创建setup_class, teardown_class 826 | 4.运行查看结果 827 | """ 828 | import pytest 829 | 830 | class TestClass(): 831 | def test_a(self): 832 | print('test_a') 833 | 834 | def test_b(self): 835 | print('test_b') 836 | 837 | def setup_class(self): 838 | print('------setup_class------') 839 | 840 | def teardown_class(self): 841 | print('------teardown_class------') 842 | 843 | if __name__ == "__main__": 844 | pytest.main(['-s', 'pytest_class.py']) 845 | 846 | 847 | """ 848 | PS E:\学习\测试\InterAutoTest_W\testcase\t_pytest> python pytest_class.py 849 | =============== test session starts ================= 850 | platform win32 -- Python 3.7.3, pytest-4.3.1, py-1.8.0, pluggy-0.9.0 851 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 852 | plugins: remotedata-0.3.1, openfiles-0.3.2, doctestplus-0.3.0, arraydiff-0.3 853 | collected 2 items 854 | 855 | pytest_class.py ------setup_class------ 856 | test_a 857 | .test_b 858 | .------teardown_class------ 859 | 860 | ============================ 2 passed in 0.04 seconds ============================== 861 | """ 862 | ``` 863 | 864 | 865 | ### 3. 常用插件: 866 | - 常用插件: https://plugincompat.herokuapp.com 867 | #### 3.1 测试报告: 868 | - 应用场景: 869 | - 自动化测试脚本最终是通过还是不通过,需要通过测试报告进行提现. 870 | - 安装: 871 | ```shell 872 | pip install pytest-html 873 | ``` 874 | - 使用 875 | - 在配置文件中的命令行参数中增加 --html=用户路径/report.html 876 | 877 | ```ini 878 | ./pytest.ini 879 | [pytest] 880 | addopts = --html=./report/report.html 881 | ``` 882 | 执行测试脚本,生成测试脚本 883 | 884 | #### 3.2 失败重试: 885 | - 应用场景: 886 | - 当失败后尝试再次运行 887 | - 安装: 888 | ```shell 889 | pip install pytest-rerunfailures 890 | ``` 891 | - 使用: 892 | - 在配置文件中的命令行参数中增加 --reruns n (n表示重试的次数) 893 | - 如果期望加上出错重试等待的时间, --rerun-delay 894 | 895 | ```ini 896 | ./pytest.ini 897 | 898 | [pytest] 899 | addopts = --html=./report/report.html --reruns 3 --reruns-delay=2 900 | ``` 901 | 902 | 903 | ```python 904 | # 使用装饰器,控制单个测试用例的运行情况 905 | @pytest.mark.flaky(reruns=3, reruns_delay=2) 906 | def test_b(): 907 | print('---test_b---') 908 | assert func(3) == 4 # 断言成功 909 | ``` 910 | 在单个测试用例中设置运行控制,则不需要在配置文件中进行配置. 911 | 912 | ### 4. 数据参数化: 913 | #### 4.1 传入单个参数: 914 | @pytest.mark.parametrize(argnames, argvalues) 915 | - argnames: 参数名; 916 | - argvalues:参数对应值,类型必须为可迭代类型,一般为list 917 | ```python 918 | # coding=utf-8 919 | """ 920 | 1. 创建类和测试方法 921 | 2. 创建数据 922 | 3. 创建参数化 923 | 4.运行 924 | """ 925 | import pytest 926 | 927 | class TestClass(): 928 | data_list = ['xiaoming', 'xiaohong'] 929 | 930 | @pytest.mark.parametrize('name',data_list) 931 | def test_a(self, name): 932 | print('test_a') 933 | print(name) 934 | assert 1 935 | 936 | 937 | if __name__ == "__main__": 938 | pytest.main(['-s','pytest_one.py']) 939 | 940 | ''' 941 | PS E:\学习\测试\InterAutoTest_W\testcase\t_pytest> python .\pytest_one.py 942 | ============== test session starts ================================= 943 | platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1 944 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 945 | plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0 946 | collected 2 items 947 | 948 | pytest_one.py test_a 949 | xiaoming 950 | .test_a 951 | xiaohong 952 | . 953 | 954 | --------- generated html file: file://E:\学习\测试\InterAutoTest_W\testcase\t_pytest\report\report.html --------- 955 | =================== 2 passed in 0.05s ================= 956 | ''' 957 | ``` 958 | 959 | 960 | #### 4.2 传入多个参数: 961 | @pytest.mark.parametrize(('参数1','参数2'), ([参数1_list], [参数2_list])) 962 | - list的每个元素都是一个元组,元组里的每个元素和参数是按顺序一一对应的 963 | 964 | ```python 965 | # coding=utf-8 966 | """ 967 | 1. 创建类和测试方法 968 | 2. 创建数据 969 | 3. 创建参数化 970 | 4.运行 971 | """ 972 | import pytest 973 | 974 | class TestClass(): 975 | data_list = [('xiaoming', '12345'),('xiaohong', '56789')] 976 | 977 | @pytest.mark.parametrize(('name','psw'),data_list) 978 | def test_a(self, name,psw): 979 | print('test_a') 980 | print(name,psw) 981 | assert 1 982 | 983 | 984 | if __name__ == "__main__": 985 | pytest.main(['-s','pytest_more.py']) 986 | 987 | 988 | ''' 989 | PS E:\学习\测试\InterAutoTest_W\testcase\t_pytest> python .\pytest_more.py 990 | ======================= test session starts ====================== 991 | platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1 992 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 993 | plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0 994 | collected 2 items 995 | 996 | pytest_more.py test_a 997 | xiaoming 12345 998 | .test_a 999 | xiaohong 56789 1000 | . 1001 | 1002 | ------- generated html file: file://E:\学习\测试\InterAutoTest_W\testcase\t_pytest\report\report.html ---------------- 1003 | ===================== 2 passed in 0.06s ============================ 1004 | ''' 1005 | ``` 1006 | 1007 | ### 5.应用接口用例: 1008 | - 登录; 1009 | - 个人信息; 1010 | - 获取商品信息; 1011 | - 购物车; 1012 | - 订单; 1013 | 1014 | #### 5.1 pytest运行原则: 1015 | ##### 5.2.1 默认规则: 1016 | - 在不指定运行目录,运行文件,运行函数等参数的默认情况下, pytest会执行当前目录下所有的以test为前缀(test*.py)或以_test为后缀(_test.py)的文件中以test为前缀的函数. 1017 | 1018 | ```python 1019 | # coding=utf-8 1020 | 1021 | """ 1022 | 1. 根据默认运行原则,调整py文件命名,函数命名 1023 | 2. pytest.main()运行,或者命令行直接运行 1024 | """ 1025 | 1026 | import sys 1027 | 1028 | sys.path.append('../') 1029 | import pytest 1030 | import requests 1031 | from utils.RequestsUtil import Request, r_get,r_post 1032 | 1033 | request = Request(url='http://211.103.136.242:8064') 1034 | 1035 | def test_login(): 1036 | """ 1037 | 登录 1038 | """ 1039 | url = 'http://211.103.136.242:8064/authorizations/' 1040 | data = {'username':'python', 'password':'12345678'} 1041 | 1042 | r = r_post(url, json=data) 1043 | print(r) 1044 | 1045 | """ 1046 | PS E:\学习\测试\InterAutoTest_W\testcase> python .\test_mall.py 1047 | ============================ test session starts ======================================== 1048 | platform win32 -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.1 1049 | rootdir: E:\学习\测试\InterAutoTest_W, inifile: pytest.ini 1050 | plugins: arraydiff-0.3, doctestplus-0.3.0, html-2.0.1, metadata-1.8.0, openfiles-0.3.2, remotedata-0.3.1, rerunfailures-8.0 1051 | collected 1 item 1052 | 1053 | test_mall.py {'code': 200, 'body': {'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Ijk1MjY3MzYzOEBxcS5jb20iLCJleHAiOjE1NzQ4NTAyMzIsInVzZXJuYW1lIjoicHl0aG9uIiwidXNlcl9pZCI6MX0.GLoT8ncQu9Pd74x0EoVjiXdnKED6JsB4WkasS8d6aPw', 'username': 'python', 'user_id': 1}} 1054 | . 1055 | 1056 | --------- generated html file: file://E:\学习\测试\InterAutoTest_W\testcase\report\report.html ------------- 1057 | ========================= 1 passed in 2.34s ========================= 1058 | """ 1059 | 1060 | ``` 1061 | 1062 | ##### 5.2.2 自定义规则运行: 1063 | 使用pytest.ini文件配置: 1064 | ```ini 1065 | ./pytest.ini 1066 | addopts = -s 1067 | # 运行参数 1068 | 1069 | testpaths = testcases 1070 | # 当前目录下的script文件夹, -可自定义 1071 | 1072 | python_files = test_*.py 1073 | # 当前目录下的script文件夹下,以test_开头,以.py结尾的所有文件 1074 | 1075 | python_classes = Test_* 1076 | # 当前目录下的script文件夹下,以test_开头,以.py结尾的所有文件, 以Test_开头的类 -可自定义 1077 | 1078 | python_functions = test_* 1079 | # 当前目录下的script文件夹下,以test_开头,以.py结尾的所有文件, 以Test_开头的类, 以test_开头的方法 -可自定义 1080 | ``` 1081 | 1082 | ## 七、结果断言: 1083 | ### 1.结果断言验证: 1084 | #### 1.1 常用断言: 1085 | - 介绍: 1086 | - 断言是自动化最终的目的,一个用例没有断言,就失去了自动化测试的意义了; 1087 | - 断言用到的是assert关键字; 1088 | - 预期结果和实际结果做对比; 1089 | - 常用断言: 1090 | 1091 | |情况|语句| 1092 | |-|-| 1093 | |判断x为真|assert x| 1094 | |判断x不为真|assert not x| 1095 | |判断b包含a| assert a in b| 1096 | |判断a等于b|assert a == b| 1097 | |判断a不等于b|assert a != b| 1098 | 1099 | #### 1.2断言应用接口用例: 1100 | - 返回状态码: 1101 | - 结果验证: 1102 | 1103 | #### 1.3断言封装 1104 | 1105 | ```python 1106 | ./utils/AssertUtil.py 1107 | # coding=utf-8 1108 | """ 1109 | 1. 定义封装类; 1110 | 2. 初始化数据,日志 1111 | 3. code相等 1112 | 4. body相等 1113 | 5.body包含 1114 | """ 1115 | import json 1116 | from utils.LogUItil import my_log 1117 | 1118 | class AssertUtil(): 1119 | 1120 | def __init__(self): 1121 | self.log = my_log('AssertUtil') 1122 | 1123 | def assert_code(self, code, expected_code): 1124 | """ 1125 | 验证返回状态码 1126 | """ 1127 | try: 1128 | assert int(code) == int(expected_code) 1129 | return True 1130 | except Exception as e: 1131 | self.log.error(f'code error, code is {code}, expected_code is {expected_code}') 1132 | raise 1133 | 1134 | def assert_body(self, body, expected_body): 1135 | """ 1136 | 验证返回结果内容相等 1137 | """ 1138 | try: 1139 | assert body == expected_body 1140 | return True 1141 | except Exception as e: 1142 | self.log.error(f'body error, body is {body}, expected_body is {expected_body}') 1143 | raise 1144 | 1145 | def assert_in_body(self, body, expected_body): 1146 | """ 1147 | 验证返回结果是否包含期望的结果 1148 | """ 1149 | try: 1150 | assert json.dumps(body) in json.dumps(expected_body) 1151 | return True 1152 | except Exception as e: 1153 | self.log.error(f'body error, body not in expected_body, body is {body}, expected_body is {expected_body}') 1154 | raise 1155 | ``` 1156 | 1157 | ### 2. 数据库结果断言验证: 1158 | #### 2.1 pymysql安装及简单实用: 1159 | - 安装: 1160 | ```shell 1161 | pip install pymysql 1162 | ``` 1163 | - 简单实用: 1164 | ```python 1165 | # coding=utf-8 1166 | """ 1167 | 1. 导入pymysql 1168 | 2. 链接database 1169 | 3. 获取执行sql的光标对象 1170 | 4. 执行sql 1171 | 5. 关闭对象 1172 | """ 1173 | import pymysql 1174 | 1175 | cnn = pymysql.connect( 1176 | host='211.103.136.242', 1177 | port=7090, 1178 | user='test', 1179 | password='test123456', 1180 | database='meiduo', 1181 | charset='utf8', 1182 | ) 1183 | 1184 | with cnn.cursor() as cursor: 1185 | sql_str = 'select * from tb_users' 1186 | cursor.execute(sql_str) 1187 | res = cursor.fetchall() 1188 | print(res) 1189 | 1190 | cnn.close() 1191 | ``` 1192 | #### 2.2 工具类封装及使用: 1193 | - pymysql工具类封装: 1194 | 1195 | ```python 1196 | ./utils/Mysqlutil.py 1197 | # coding=utf-8 1198 | """ 1199 | 1. 创建封装类 1200 | 2.初始化数据,连接数据库,光标对象 1201 | 3. 创建查询,执行方法 1202 | 4. 关闭对象 1203 | """ 1204 | import pymysql 1205 | import functools 1206 | 1207 | from utils.LogUItil import my_log 1208 | 1209 | class Mysql(): 1210 | def __init__(self,host,user,password,database,port=3306,charset='utf8'): 1211 | self.cnn = pymysql.connect( 1212 | host=host, 1213 | port=port, 1214 | user=user, 1215 | password=password, 1216 | database=database, 1217 | charset=charset 1218 | ) 1219 | self.cursor = self.cnn.cursor() 1220 | self.log = my_log() 1221 | 1222 | def __del__(self): 1223 | try: 1224 | self.cnn.close() 1225 | except Exception as e: 1226 | pass 1227 | 1228 | try: 1229 | self.cursor.close() 1230 | except Exception as e: 1231 | pass 1232 | 1233 | def fetch_one(self, sql): 1234 | """ 1235 | 查询一个对象 1236 | """ 1237 | self.cursor.execute(sql) 1238 | return self.fetchone() 1239 | 1240 | def fetch_all(self, sql): 1241 | """ 1242 | 查询全部对象 1243 | """ 1244 | self.cursor.execute(sql) 1245 | return self.fetchall() 1246 | 1247 | def exec(self,sql): 1248 | """ 1249 | 执行操作 1250 | """ 1251 | try: 1252 | self.cursor.execute(sql) 1253 | self.cursor.commit() 1254 | except Exception as e: 1255 | self.cnn.rollback() 1256 | self.log.error(e) 1257 | return False 1258 | return True 1259 | 1260 | if __name__ == "__main__": 1261 | mysql = Mysql( 1262 | host='211.103.136.242', 1263 | port=7090, 1264 | user='test', 1265 | password='test123456', 1266 | database='meiduo', 1267 | charset='utf8', 1268 | ) 1269 | res = mysql.fetch_all('select * from tb_uders') 1270 | print(res) 1271 | ``` 1272 | 1273 | - 配置文件: 1274 | 1275 | ```yml 1276 | # ./config/db_conf.yml 1277 | db_1: 1278 | db_host: "211.103.136.242" 1279 | db_port: 7090 1280 | db_user: "test" 1281 | db_password: "test123456" 1282 | db_database: "meiduo" 1283 | db_charset: "utf8" 1284 | 1285 | ``` 1286 | 1287 | ```python 1288 | ./config/Conf.py 1289 | """ 1290 | 1. 创建db_conf.yml 1291 | 2. 编写数据库基本信息 1292 | 3. 重构Conf.py 1293 | 4. 执行 1294 | """ 1295 | 1296 | ... 1297 | 1298 | # 定义db_conf.yml文件路径 1299 | _db_config_file = _config_path + os.sep + 'db_conf.yml' 1300 | 1301 | def get_db_conf_file(): 1302 | """ 1303 | 获取db_conf文件路径 1304 | """ 1305 | return _db_config_file 1306 | 1307 | 1308 | # 2. 读取配置文件 1309 | class ConfigYaml(): 1310 | def __init__(self): 1311 | self.config = YamlReader(get_config_file()).data() 1312 | self.db_config = YamlReader(get_db_conf_file()).data() 1313 | 1314 | def get_db_conf_info(self, db_alias): 1315 | """ 1316 | 根据db_alias获取数据库相关参数 1317 | """ 1318 | return self.db_config[db_alias] 1319 | 1320 | if __name__ == "__main__": 1321 | conf_read = ConfigYaml() 1322 | ...... 1323 | print(conf_read.get_db_conf_info('db_1')) 1324 | ``` 1325 | 1326 | - 数据库结果验证: 1327 | - 1. 初始化数据库信息,Base.py, init_db 1328 | - 2. 接口用例返回结果内容进数据库验证 1329 | ```python 1330 | # ./common/Base.py 1331 | 1332 | # coding=utf-8 1333 | # 1.定义一个方法init_db 1334 | # 2.初始化数据库信息, 通过配置文件来完成 1335 | # 3.初始化mysql对象 1336 | 1337 | from config.Conf import ConfigYaml 1338 | from utils.MysqlUtil import Mysql 1339 | 1340 | 1341 | def init_db(db_alias): 1342 | db_init = ConfigYaml().get_db_conf_info(db_alias) 1343 | host = db_init.get('host', '127.0.0.1') 1344 | port = int(db_init.get('port', 3306)) 1345 | user = db_init.get('user') 1346 | database = db_init.get('database') 1347 | password = db_init.get('password') 1348 | charset = db_init.get('charset', 'utf8') 1349 | 1350 | conn = Mysql(host=host,port=port,user=user,password=password,database=database,charset=charset) 1351 | return conn 1352 | 1353 | if __name__ == "__main__": 1354 | conn = init_db('db_1') 1355 | print(conn) 1356 | ``` 1357 | 1358 | ## 八、数据驱动: 1359 | ### 1. yaml数据驱动: 1360 | #### 1.1 yaml测试用例: 1361 | ```yml 1362 | #./data/testlogin.yml 1363 | # 登录的测试用例: 1364 | 1365 | # 测试名称: 1366 | # url地址 1367 | # data 1368 | # 期望结果 1369 | --- 1370 | 'case_name':"登陆成功用例" 1371 | 'url': 'http://211.103.136.242:8064/authorizations/' 1372 | 'data': 1373 | 'username': "python" 1374 | 'password': "12345678" 1375 | 'expect': "'username': 'python', 'user_id': 1" 1376 | --- 1377 | 'case_name':"登陆失败用例" 1378 | 'url': 'http://211.103.136.242:8064/authorizations/' 1379 | 'data': 1380 | 'username': "test123456" 1381 | 'password': "1231111" 1382 | 'expect': "'username': 'python', 'user_id': 1" 1383 | 1384 | ``` 1385 | 1386 | #### 1.2 参数化: 1387 | ```python 1388 | # ./testcase/test_login.py 1389 | 1390 | # coding=utf-8 1391 | 1392 | # 1. 获取测试用例的列表 1393 | # 获取testlogin.yml文件路径 1394 | # 使用工具类来读取多个文档的内容 1395 | # 2. 参数化执行测试用例 1396 | 1397 | import os 1398 | import sys 1399 | sys.path.append("../") 1400 | import pytest 1401 | from config import Conf 1402 | from config.Conf import ConfigYaml 1403 | from utils.YamlUtil import YamlReader 1404 | from utils.RequestsUtil import Request 1405 | 1406 | test_file = os.path.join(Conf.get_data_path(), 'testlogin.yml') 1407 | print(test_file) 1408 | 1409 | data_list = YamlReader(test_file).data_all() 1410 | print(data_list) 1411 | 1412 | @pytest.mark.parametrize("login", data_list) 1413 | def test_yaml(login): 1414 | """ 1415 | 执行测试用例 1416 | """ 1417 | uri = login['url'] 1418 | print(uri) 1419 | data = login['data'] 1420 | print(data) 1421 | request = Request(ConfigYaml().get_config_url()) 1422 | res = request.post(uri, json=data) 1423 | print(res) 1424 | 1425 | 1426 | if __name__ == "__main__": 1427 | pytest.main(['test_login.py']) 1428 | ``` 1429 | 1430 | ### 2. Excel数据驱动 1431 | #### 2.1 excel用例设计: 1432 | (./data/testdata.xlsx) 1433 | #### 2.2 excel读取: 1434 | ```python 1435 | # coding=utf-8 1436 | 1437 | """ 1438 | 1. 导入包, xlrd(python自带) 1439 | 2. 创建workbook对象 1440 | 3. sheet对象 1441 | 4. 获取行数和列数 1442 | 5. 读取每行的内容 1443 | 6. 读取每列的内容 1444 | 7. 读取固定列的内容 1445 | """ 1446 | import xlrd 1447 | book = xlrd.open_workbook('./testdata.xlsx') 1448 | # 获取表的两种方式: 1449 | # 索引 1450 | # sheet = book.sheet_by_index(0) 1451 | # 名称 1452 | sheet = book.sheet_by_name('美多商城接口测试') 1453 | 1454 | rows = sheet.nrows # 行数 1455 | cols = sheet.ncols # 列数 1456 | print(f"rows:{rows}, cols:{cols}") 1457 | 1458 | # 获取每行数据 1459 | for r in range(rows): 1460 | r_values = sheet.row_values(r) 1461 | print(r_values) 1462 | 1463 | # 获取每列数据 1464 | for c in range(cols): 1465 | c_values = sheet.col_values(c) 1466 | print(c_values) 1467 | 1468 | # 读取固定列的内容 1469 | v = sheet.cell(1,2) 1470 | print(v) 1471 | ``` 1472 | #### 2.3 封装excel工具类: 1473 | 1474 | ```python 1475 | # ./utils/ExcelUtil.py 1476 | 1477 | # coding=utf-8 1478 | # 目的: 参数化, pytest list 1479 | 1480 | # 1. 验证文件是否存在,存在读取,不存在错报 1481 | # 2. 读取sheet方式, 名称,索引 1482 | # 3. 读取sheet内容 1483 | # 返回list, 字典 1484 | # 格式: [{'a':"a1",'b':" b1"}, {'a':"a2",'b':"b2"}] 1485 | # 4. 结果返回 1486 | 1487 | import os 1488 | import xlrd 1489 | 1490 | # 自定义异常 1491 | class SheetTypeError(object): 1492 | pass 1493 | 1494 | class ExcelReader(): 1495 | def __init__(self, excel_file, sheet_by): 1496 | if os.path.exists(excel_file): 1497 | self.excel_file = excel_file 1498 | self.sheet_by = sheet_by 1499 | self.data_list = [] 1500 | else: 1501 | raise FileNotFoundError("文件不存在") 1502 | 1503 | def data(self): 1504 | if self.data_list: 1505 | return self.data_list 1506 | 1507 | workbook = xlrd.open_workbook(self.excel_file) 1508 | 1509 | if type(self.sheet_by) not in [str,int]: 1510 | raise SheetTypeError("参数错误") 1511 | elif type(self.sheet_by) == int: 1512 | sheet = workbook.sheet_by_index(self.sheet_by) 1513 | elif type(self.sheet_by) == str: 1514 | sheet = workbook.sheet_by_name(self.sheet_by) 1515 | 1516 | # 获取首行信息 1517 | title = sheet.row_values(0) 1518 | for r in range(1, sheet.nrows): 1519 | self.data_list.append(dict(zip(title,sheet.row_values(r)))) 1520 | # print(self.data_list) 1521 | return self.data_list 1522 | 1523 | if __name__ == "__main__": 1524 | excel_reader = ExcelReader('../data/testdata.xlsx',"美多商城接口测试") 1525 | print(excel_reader.data()) 1526 | 1527 | ``` 1528 | 1529 | 1530 | #### 2.4 excel参数化运行: 1531 | - 获取是否运行; 1532 | - 参数化; 1533 | - 结果断言; 1534 | 1535 | ```python 1536 | # ./common/ExcelConfig.py 1537 | # coding=utf-8 1538 | 1539 | # 定义类 1540 | # 定义列属性 1541 | # 定义excel的映射 1542 | 1543 | class DataConfig(): 1544 | # 用例属性: 1545 | case_id = "用例ID" 1546 | case_model = "模块" 1547 | case_name = "接口名称" 1548 | url = "请求URL" 1549 | pre_exec = "前置条件" 1550 | method = "请求类型" 1551 | params_type = "请求参数类型" 1552 | params = "请求参数" 1553 | expect_result = "预期结果" 1554 | actual_result = "实际结果" 1555 | beizhu = "备注" 1556 | is_run = "是否运行" 1557 | headers = "headers" 1558 | cookies = "cookies" 1559 | code = "status_code" 1560 | db_verify = "数据库验证" 1561 | ``` 1562 | 1563 | ```python 1564 | # ./config/ExcelData.py 1565 | # coding=utf-8 1566 | 1567 | # 1. 使用excel工具类, 获取结果list 1568 | # 2. 列"是否运行内容", y 1569 | # 3. 保存要执行结果, 放到新的列表中 1570 | import sys 1571 | sys.path.append('../') 1572 | from utils.ExcelUtil import ExcelReader 1573 | from common.ExcelConfig import DataConfig 1574 | 1575 | class Data(): 1576 | def __init__(self, excel_file, sheet_by): 1577 | self.reader = ExcelReader(excel_file, sheet_by) 1578 | self.run_list = [] 1579 | 1580 | def get_run_data(self): 1581 | """ 1582 | 根据"是否运行"列,获取执行测试用例 1583 | """ 1584 | for line in self.reader.data(): 1585 | if str(line[DataConfig().is_run]).lower() == 'y': 1586 | self.run_list.append(line) 1587 | return self.run_list 1588 | ``` 1589 | #### 2,4 动态的headers请求 1590 | - 1) 判断header是否存在? json转义: 无需 1591 | - 2) 增加Headers 1592 | - 3) 增加cookies 1593 | - 4) 发送请求 1594 | 1595 | #### 2.5 动态关联: 1596 | - 1) 验证前置条件; 1597 | - 2) 找到执行用例; 1598 | - 3) 发送请求,获取前置用例结果; 1599 | - 发送获取前置用例; 1600 | - 数据初始化, 重构get/post 1601 | - 4) 替换headers变量; 1602 | - 验证请求中师傅含有$()$, 返回$()$内容 1603 | - 根据内容token,查询前置条件用例返回结果 1604 | - 根据变量结果内容,替换 1605 | - 5) 发送请求 1606 | 1607 | #### 2.6 断言验证: 1608 | - 状态码; 1609 | - 返回结果内容; 1610 | - 数据库相关结果的验证 1611 | - 1) 初始化数据库; 1612 | - 2) 查询sql语句; 1613 | - 3) 获取数据库结果的key; 1614 | - 4) 根据key获取数据库结果和接口结果 1615 | - 5) 验证 1616 | 1617 | ## 九、Allure报告: 1618 | ### 1.快速入门: 1619 | #### 1.1 allure安装: 1620 | - python插件: 1621 | - 命令行安装: 1622 | ```shell 1623 | pip install allure-pytest 1624 | ``` 1625 | - 源代码安装: 1626 | ```shell 1627 | htpps://pypi.org/project/allure-pytest 1628 | ``` 1629 | 1630 | - allure工具: 1631 | - 下载: 1632 | https://bintray.com/qameta/generic/allure2 1633 | https://github.com/allure-framework/allure2 1634 | - 前置条件: 1635 | - 已部署java环境 1636 | - 解压并配置系统环境变量; 1637 | 1638 | #### 1.2 allure使用: 1639 | - 配置pytest.ini 1640 | ```ini 1641 | [pytest] 1642 | # addopts = -s --html=./report/report.html --reruns 3 --reruns-delay=2 1643 | 1644 | # allure setting 1645 | addopts = -s --allure ./report/report.html 1646 | ... 1647 | ``` 1648 | - 添加allure代码; 1649 | - 运行; 1650 | - allure工具生成html报告; 1651 | ```shell 1652 | allure generate {result_path} -o {report_path} 1653 | or 1654 | allure generate {result_path} -o {report_path} --clean 1655 | ``` 1656 | 1657 | ### 2. allure详解: 1658 | 1659 | |注解|说明| 1660 | |-|-| 1661 | |Title|可以自定义用例标题, 标题默认函数名| 1662 | |Description|测试用例的详细说明| 1663 | |Feature|定义功能模块, 往下是Story| 1664 | |Story|定义用户故事| 1665 | |Severity|定义用例级别,主要有BLOCKER(拦截器), CRITICAL(关键), MINOR(次要), NORMAL(正常), TRIVIAL(琐碎)等几种类型,默认是NORMAL| 1666 | |Allure.dynamic|动态设置相关配置| 1667 | 1668 | ```python 1669 | # coding=utf-8 1670 | 1671 | import pytest 1672 | import allure 1673 | 1674 | @allure.feature('接口测试, 这是一个一级标签') 1675 | class TestAllure(): 1676 | # 定义测试方法 1677 | @allure.title('测试用例标题1') 1678 | @allure.description('执行测试用例1的结果是:test_1') 1679 | @allure.stroy("这是一个二级标签:test1") 1680 | @allure.severity(allure.severity.CRITICAL) 1681 | def test_1(self): 1682 | print("test_1") 1683 | 1684 | @allure.title('测试用例标题2') 1685 | @allure.description('执行测试用例2的结果是:test_2') 1686 | @allure.stroy("这是一个二级标签:test1") 1687 | @allure.severity(allure.severity.BLOCKER) 1688 | def test_2(self): 1689 | print("test_2") 1690 | 1691 | @allure.title('测试用例标题3') 1692 | @allure.description('执行测试用例3的结果是:test_3') 1693 | @allure.stroy("这是一个二级标签:test3") 1694 | def test_3(self): 1695 | print("test_3") 1696 | 1697 | @pytest.mark.parametrize("case",['case1', 'case2', 'case3']) 1698 | def test_4(self, case): 1699 | print(f"test4: {case}") 1700 | allure.dynamic.title(case) 1701 | 1702 | 1703 | if __name__ == "__main__": 1704 | pytest.main(['allure_demo.py']) 1705 | ``` 1706 | 1707 | ### 3. Allure应用: 1708 | #### 3.1 应用测试用例: 1709 | ##### 1) 区分层级: 1710 | - sheet名称 --> feature一级标签 1711 | - 模块 --> story 二级标签 1712 | - 用例id+接口名称 --> title 1713 | - 请求url,请求类型,期望结果, 实际结果描述 1714 | 1715 | ```python 1716 | # ./tesecase/test_excel_case.py 1717 | 1718 | ... 1719 | class TestExcel(): 1720 | ... 1721 | 1722 | @pytest.mark.parametrize('case', run_list) 1723 | def test_run(self,case): 1724 | ... 1725 | print(f"执行测试用例:{res}") 1726 | 1727 | # allure 1728 | allure.dynamic.feature(sheet_name) 1729 | allure.dynamic.story(case_model) 1730 | allure.dynamic.title(case_id + case_name) 1731 | desc = f"url:{url}
请求方法:{method}
期望结果:{expect_result}
实际结果:{res}" 1732 | allure.dynamic.description(desc) 1733 | 1734 | 1735 | # 断言验证 1736 | ... 1737 | ``` 1738 | ##### 2) 自动生成测试报告: 1739 | *不用再手动执行生成allure的html报告* 1740 | - subprocess介绍: 1741 | - 允许生产新的进程,并且可以把输入,输出,错误直接连接到管道,最后获取结果 1742 | - 官方网站: 1743 | - https://docs.python.org/3.8/library/subprocess.html 1744 | - 方法: 1745 | - subprocess.all(): 父进程等待子进程完成, 返回退出信息(returncode, 相当于linux exit code) 1746 | - shell=True的用法, 支持命令以字符串的形式传入 1747 | 1748 | ```python 1749 | # ./common/Base.py 1750 | 1751 | def allure_report(report_path): 1752 | allure_cmd = f"allure generate {report_path}/result -o {report_path}/html --clean" 1753 | log.info(f"报告地址:{report_path}") 1754 | try: 1755 | subprocess.call(allure_cmd, shell=True) 1756 | except Exception as e: 1757 | log.error(e) 1758 | ``` 1759 | 1760 | #### 3.2 项目运行: 1761 | 1762 | ## 十、邮件配置: 1763 | ### 1. 配置文件设置及邮件封装: 1764 | (略) 1765 | ### 2. 邮件运行: 1766 | (略) 1767 | 1768 | # 三、持续集成与docker介绍及配置: 1769 | ## 1. jenkins和docker介绍及安装: 1770 | ### 1.1 docker: 1771 | #### 1) 介绍: 1772 | - 开源免费; 1773 | - 方便快速搭建环境; 1774 | - 自动化测试和持续集成,发布; 1775 | 1776 | #### 2)安装: 1777 | - 流程:(基于centos) 1778 | - 1. 查看内核版本; 1779 | - 2. 安装需要的软件包; 1780 | - 3. 设置yum源; 1781 | - 4. 查询及安装docker; 1782 | - 5. 启用docker, 并加入开机启动; 1783 | 1784 | #### 3) docker拉取镜像慢: 1785 | *将镜像源修改为阿里云镜像源* 1786 | - 1. 编辑docker配置文件: 1787 | ```shell 1788 | vi /lib/systemd/system/docker.service 1789 | ``` 1790 | - 2. 修改配置内容: 1791 | ```shell 1792 | ExecStart=/usr/bin/docker 1793 | 修改为: 1794 | ExecStart=/usr/bin/docker --registry-mirror=https://u1qbyfsc.mirror.aliyuncs.com 1795 | ``` 1796 | 1797 | #### 4) docker的基本用法: 1798 | 1799 | |常用命令|说明| 1800 | |-|-| 1801 | |docker images|列出本地主机上的镜像| 1802 | |docker ps -a| 查看容器状态| 1803 | |docker start(stop,restart) container-name|容器启动(关闭,重启)命令| 1804 | |docker exec|进入正在后台执行的容器(参数:-d: 分离模式,在后台执行; -i: 即使没有附加也保持STDIN打开; -t: 分配一个伪终端)| 1805 | 1806 | 1807 | ### 1.2 jenkins: 1808 | #### 1) 介绍: 1809 | - 开源免费; 1810 | - 安装配置简单; 1811 | - 跨平台,支持所有平台; 1812 | - web形式的可视化管理页面; 1813 | - 分布式构建; 1814 | - 丰富的插件支持; 1815 | 1816 | #### 2) 安装: 1817 | *系统: centos; 容器: docker* 1818 | - 1. 选择docker版本安装; 1819 | - https://jenkins.io/zh/download/ 1820 | - 2. 安装; 1821 | - 3. 查看下载完的镜像; 1822 | - 4. 启动jenkins镜像; 1823 | - 5. 浏览http://localhost 并等待*Unlock Jenkins*页面出现; 1824 | - 6. 使用后面的步骤设置向导完成设置; 1825 | 1826 | #### 3) 启动jenkins: 1827 | ```shell 1828 | docker run -d -p 80:8080 -p 50000:50000 -v jenkins:/var/jenkins_home -v /etc/localtime:/etc/localtime --name jenkins docker.io/jenkins/jenkins:lts 1829 | ``` 1830 | - 启动参数意义: 1831 | 1832 | |参数|意义| 1833 | |-|-| 1834 | |-d| 后台运行| 1835 | |-p 80:8080|将镜像的8080端口映射到服务器的80端口| 1836 | |-p 50000:50000|将镜像的50000端口映射到服务器的50000端口| 1837 | |-v jenkins:/var/jenkins_home|/var/jenkins_home目录为jenkins工作目录,将硬盘上的一个目录挂载到这个位置, 方便后续更新镜像后继续使用原来的工作目录| 1838 | |-v /etc/localtime:/etc/localtime|让容器使用和服务器使用同样的时间设置| 1839 | |--name jenkins|给容器起个名| 1840 | 1841 | ## 2. Jenkins插件安装及配置: 1842 | ### 2.1 allure: 1843 | #### 1)安装: 1844 | - 1. 进入jenkins -> 系统管理 -> 管理插件 -> 可选插件 1845 | - 2. 搜索框输入"allure" -> 选中点击"直接安装" -> 等待安装完成 -> 重启jenkins 1846 | 1847 | #### 2) 配置(Allure Commandline): 1848 | - 设置路径: 1849 | - 系统设置 -> 全局工具配置 -> Allure Commandline 1850 | 1851 | - 1. Allure下载地址: 1852 | - https://dl.bintray.com/qameta/generic/io/qameta/allure/2.7.0 1853 | - 2. Allure安装: 1854 | - 解压到本地, 配置环境变量即可. 1855 | 1856 | 1857 | ### 2.2 git: 1858 | #### 1)安装: 1859 | git是默认安装的 1860 | 1861 | 1862 | #### 2) 配置: 1863 | - 1) 系统管理 -> 全局工具配置 -> git 1864 | - 2) 点击"添加" -> 输入name,path 1865 | 1866 | 1867 | 1868 | ## 3. jenkins持续集成配置及运行: 1869 | ### 3.1 general: 1870 | #### 1) 创建项目 1871 | - 1. 进入jenkins主页; 1872 | - 2. 点击"新建"; 1873 | - 3. 输入项目名称, 并点击"构建一个自由风格的软件项目"; 1874 | - 4. 点击"确定" 1875 | 1876 | 1877 | #### 2) 配置项目: 1878 | - 1. General; 1879 | - a. 节点配置: 1880 | - 系统管理 -> 节点管理 -> 新建节点 1881 | - windows为例: 1882 | - 工作目录: 工作目录 1883 | - 启动方式: "通过java web启动代理" 1884 | - b. 选择"在必要的时候并发构建" 1885 | - c. 选择"限制项目的运行节点" -> 输入要运行的节点 1886 | - 点击"保存" 1887 | - 2. 源码管理; 1888 | - a. 选择"Git"; 1889 | - b. 输入git地址; 1890 | - c. 添加jenkins凭证认证; 1891 | - 3. 构建触发器; 1892 | - 第一颗 * 表示分钟, 取值0~59 1893 | - 第二颗 * 表示小时, 取值0~23 1894 | - 第三颗 * 表示一个月的第几天, 取值1~31 1895 | - 第四颗 * 表示一周中的第几天, 取值0~7, 其中0和7代表的都是周日 1896 | - 4. 构建配置; 1897 | - 增加构建步骤: 1898 | - windows: 1899 | - Execute Windows batch command 1900 | - linux/Mac: 1901 | - Execute shell 1902 | - 编写run.py 1903 | ```python 1904 | # ./run.py 1905 | # coding=utf-8 1906 | import os 1907 | import pytest 1908 | from config import Conf 1909 | 1910 | if __name__ == "__main__": 1911 | report_path = Conf._report_path() 1912 | pytest.main(['-s', '--alluredir', report_path+'/reslut']) 1913 | ``` 1914 | - 5. 报告; 1915 | - 1. 点击增加构建后步骤,选择"Allure Report"; 1916 | - 2. 在path中输入allure报告所在的目录名称; 1917 | - 5. 邮件; 1918 | - 1. jenkins -> 系统配置 -> 邮件通知 1919 | - 2. 设置相关信息; 1920 | - 3. 构建后的操作 -> 增加构建有的操作步骤 -> E-mail notification -> 接收邮箱 1921 | #### 3) 运行项目: 1922 | - 自动构建: 1923 | 1924 | - 手动构建: 1925 | - jenkins -> 立即构建 -> 点击进度条 -> 点击"控制台输出" -> 查看报错信息 1926 | 1927 | #### 4) 查看邮件: 1928 | 1929 | #### 5) 查看报告: 1930 | 1931 | ### 3.2 源码管理: 1932 | 1933 | ### 3.3 其他配置: 1934 | 1935 | ### 3.4 运行: 1936 | 1937 | ## 4、拓展: 1938 | ### 4.1 学习网站: 1939 | - Dokcker安装jenkins: 1940 | - https://github.com/jenkinsci/docker/blob/master/README.md 1941 | - Docker使用: 1942 | - https://www.runoob.com/docker/docker-container-usage.html 1943 | 1944 | 1945 | 1946 | 1947 | 1948 | 1949 | 1950 | --------------------------------------------------------------------------------