pytest总结

目录

pytest命令行参数

(1)--tb

(2)-n auto --dist=loadscope

(3)-m

1、allure

(1)安装allure

(2)allure使用

1、@allure.step()

2、@allure.title()

3、allure.attach()

4、@allure.description  @ allure.description_html

5、@allure.link、@allure.issue、@allure.testcase

6、allure的标记装饰器

7、@allure.severity(allure.severity_level.TRIVIAL)

(3)allure介入jenkins

1、Allure Commandline配置

2、JDK配置

3、allure report插件配置

4、html的报告

5、结果发送邮件

6、pytest+allure+jenkins

2、conftest中的固定方法

(1)pytest_collection_modifyitems(session, config,items)

items:用例对象列表

(2)pytest_addoption

3、fixture使用

(1)作用范围

(2)fixture实现setup和tearDown

(3)fixture传递测试数据

(3)参数化fixture

(4)使用方式

第一:调用方式

第二:执行顺序 

第三:自动执行

4、用例参数化

(2)pytest.mark.parametrize()

(3)yaml文件驱动

(4)id和ids使用

(5)pytest.param使用

5、marks

6、pytest处理mock

7、日志处理

8、mysql和redis的封装

9、ssh操作远程机器


pytest命令行参数

(1)--tb

--tb=auto 有多个用例失败的时候,只打印第一个和最后一个用例的回溯信息
--tb=long 输出最详细的回溯信息
--tb=short 输入assert的一行和系统判断内容
--tb=line 使用一行显示错误信息
--tb=native 只输出python标准库的回溯信息
--tb=no 不显示回溯信息

import pytest


def test_region():
    with pytest.raises(ZeroDivisionError):
        10 / 10

运行:我长时间使用了--tb=no(使用数据驱动的话很烦这个信息)

pytest总结_第1张图片 pytest总结_第2张图片

(2)-n auto --dist=loadscope

作用:并行执行用例

安装包:pip install pytest-xdist

  • -n auto:可以自动检测到系统的CPU核数;从测试结果来看,检测到的是逻辑处理器的数量
  • --dist=loadscope:将按照同一个模块module下的函数和同一个测试类class下的方法来分组,然后将每个测试组发给可以执行的worker,确保同一个组的测试用例在同一个进程中执行
  • --dist=loadfile:按照同一个文件名来分组,然后将每个测试组发给可以执行的worker,确保同一个组的测试用例在同一个进程中执行

(3)-m

作用:筛选用例

  • 命令1:pytest -m "P0" (过滤mark了P0的用例)
  • 命令2:pytest -m "P0 or P1"  (过滤mark了P0或者P1的用例)
  • 命令3:pytest -m "P0 and P1" (过滤mark了P0且P1的用例)
  • 命令4:pytest -m "P0 and not P1"   (过滤mark了P0且没有P1的用例)

1、allure

(1)安装allure

依赖java:Linux系统下安装jdk及环境配置(两种方法)_橙子君的博客-CSDN博客_linux配置jdk(liunx)

安装包:https://github.com/allure-framework/allure2/releases/download/2.13.8/allure-2.13.8.zip

放在那里看自己心情(liunx、windows)下

windows下:allure安装配置 - 简书

liunx下:配置软连接方式添加allure命令------ln -s  /data/hanfang/.virtualenvs/shijian/allure-2.13.8/bin/allure   /usr/bin/allure   (第一个path是allure的解压路径) 

             验证allure --version

           

辅助包:pip install allure-pytest(实际应用中pytest安装辅助包即可不需要单独安装allure,但是nosetest需要安装allure

异常处理:

            报错提示AttributeError: module 'allure' has no attribute 'severity_level'或者下图

pytest总结_第3张图片

             pip list |grep allure

             pip uninstall pytest-allure-adaptor2(就是这个东西的祸端,有时候你安装的是pytest-allure-adaptor就卸载它)

                       这里特别说明下:pytest-allure-adaptor2 和pytest-allure-adaptor这两个包可以生成xml格式的结果(如果需要xml结果的报告时)

                            重点:1、和allure-pytest不能同时存在(nose使用nose-allure-plugin和pytest-allure-adaptor两个包,pytest使用allure-pytest)

                                       2、adaptor不支持allure的很多标签,例如epic

                                                             pytest总结_第4张图片

             pip install allure-pytest

(2)allure使用

pytest总结_第5张图片

第一步:将以上用例输出测试报告:pytest tmpdir_tmpdir_factory.py --alluredir=./report  

pytest总结_第6张图片

第二步:将报告转化为html

allure generate ./report -o ./html --clean   :将./report这个目录的报告转化到./html下生成html的报告, 源路径默认是allure-results(对应./report) ,转出地址默认值是allure-report(对应./html)

第三步:打开报告

pycharm在index.html右键打开在 浏览器

pytest总结_第7张图片

pytest总结_第8张图片

vscode中使用allure启动server方式:allure open html(html的文件夹就是上面生成的报告)

 pytest总结_第9张图片

环境变量展示:allure报告环境配置(environment) - xyztank - 博客园

相关参考:Jenkins执行testNG生成美观的测试报告(Allure2)_qq_15290529的博客-CSDN博客

官网说明书:Allure Framework

好的文章:allure--下的各装饰器的翻译及自己的总结 - 你是猪啊! - 博客园

1、@allure.step()

               展示步骤都干啥了,层级和入参有助于分析.

pytest总结_第10张图片

和入参关联动态变化

pytest总结_第11张图片

# python中关键字参数和位置参数获取方式
import allure


@allure.step("这样的找到确认这个p1一定有就行---模板名:{p1}")
def get_preset_detail(**kwargs):
    p1 = kwargs.get("p1")


def test_preset_detail():
    get_preset_detail(p1="lala")

2、@allure.title()

         和step用法一样,展示的位置不同

pytest总结_第12张图片pytest总结_第13张图片

3、allure.attach()

       添加附件------图片、连接,文件等

4、@allure.description  @ allure.description_html

      方法描述,相当如方法下面的"""方法注释"""

5、@allure.link、@allure.issue、@allure.testcase

 Pytest(16) - allure参数:@allure.link()、@allure.issue()、@allure.testcase()_KeKe_Winner的博客-CSDN博客

6、allure的标记装饰器

Pytest系列(23)- allure 打标记之 @allure.epic()、@allure.feature()、@allure.story() 的详细使用 - 小菠萝测试笔记 - 博客园

# 只运行 epic 名为 test 的测试用例
pytest --alluredir ./report/allure --allure-epics=test

# 只运行 feature 名为 模块 的测试用例
pytest --alluredir ./report/allure --allure-features=模块

# 只运行 story1、story2 的测试用例(也可以不用=号 空格就行了哦)
pytest tests.py --allure-stories story1,story2

# 指定 feature+story
pytest tests.py --allure-features feature2 --allure-stories story2

7、@allure.severity(allure.severity_level.TRIVIAL)

@allure.severity(allure.severity_level.TRIVIAL)
@allure.severity(allure.severity_level.NORMAL)
@allure.severity(allure.severity_level.NORMAL)
@allure.severity(allure.severity_level.CRITICAL)

(3)allure介入jenkins

参考文章:Jenkins Allure Report 插件的相关配置_MaggieTian77的博客-CSDN博客

(省略了jenkins安装allure插件)

1、Allure Commandline配置

pytest总结_第14张图片

pytest总结_第15张图片

2、JDK配置

Allure Commandline配置的地方一样

pytest总结_第16张图片

3、allure report插件配置

pytest总结_第17张图片

jenkins没有其他处理,就是在指定的目录运行nosetests命令后就到了allure Report

pytest总结_第18张图片

执行完了的 结果

pytest总结_第19张图片

4、html的报告

pytest总结_第20张图片

pytest总结_第21张图片pytest总结_第22张图片

5、结果发送邮件

pytest总结_第23张图片

pytest总结_第24张图片

6、pytest+allure+jenkins

 与nosetest框架不同点:

(1)allure报告结果会左右jenkins的任务的状态

可以忽略

pytest总结_第25张图片

 (2)会出现pytest和Jenkins环境变量不一致情况

       --metadata-from-json "{}"

(3)pytest产生html报告

安装包:pip install pytest-html 

命令:--html=report.html --self-contained-html

2、conftest中的固定方法

(1)pytest_collection_modifyitems(session, config,items)

items:用例对象列表

           用例运行后展示------个人认为需要用

# conftest.py文件内容
def pytest_collection_modifyitems(items):
    """
    测试用例收集完成时,将收集到的item的name和nodeid的中文显示在控制台上
    """
    for item in items:
        item.name = item.name.encode("utf-8").decode("unicode_escape")
        item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
        if item.obj.__doc__:
            item._nodeid += ":" + item.obj.__doc__.strip()



# 测试文件内容
# -*- coding:utf-8 -*-

from contextlib import contextmanager
import pytest


@contextmanager
def does_not_raise():
    yield


@pytest.mark.parametrize(
    "example_input,expectation",
    [
        pytest.param(3, does_not_raise(), id="除数是3的情况"),
        pytest.param(2, does_not_raise(), id="除数是2的情况"),
        pytest.param(1, does_not_raise(), id="除数是1的情况"),
        pytest.param(0, pytest.raises(ZeroDivisionError), id="除数是0的情况"),
    ],
)
def test_division(example_input, expectation):
    """中文怎么办"""
    with expectation:
        assert (6 / example_input) is not None

pytest总结_第26张图片

   用例执行顺序------必要性不大  (参考文章pytest文档33-Hooks函数获取用例执行结果(pytest_runtest_makereport) - 上海-悠悠 - 博客园

# conftest.py内容
def pytest_collection_modifyitems(items):
    # 将用例名拿出来存入新列表-利用用例名找到items的索引位置才能操作items这个列表
    new_items = []
    for item in items:
        new_items.append(item.name)
 
    # 1. 删除 test_A_002 用例
    # 获取 test_A_002 在新列表的索引
    index_2 = new_items.index('test_A_002')
    # 在老列表中删除这个索引
    del items[index_2]
    del new_items[index_2]  # 新列表同步删除,和老列表保持同步
 
    # 2. 调换 1 和 3 的顺序
    # 获取 1 和 3 在新列表的索引
    index_1 = new_items.index('test_A_001')
    index_3 = new_items.index('test_A_003')
    # 根据索引在老列表中调换位置
    items[index_1], items[index_3] = items[index_3], items[index_1]

# 排序的另一个方法实现,按照用例名称排序,是不是也可以根据id的顺序??给id带上序号
def pytest_collection_modifyitems(session, items):
    print(type(items))
    print("收集到的测试用例:%s" % items)
    # sort排序,根据用例名称item.name 排序
    items.sort(key=lambda x: x.name)
    print("排序后的用例:%s" % items)
    for item in items:
        print("用例名:%s" % item.name)


#用例
def test_A_001():
    pass


def test_A_002():
    pass


def test_A_003():
    pass

 pytest总结_第27张图片

          动态给用例添加mark功能

def pytest_collection_modifyitems(items):
    """动态根据方法名添加mark,酸爽啊,名字就是mark"""
    for item in items:
        print(item.name)
        print(item.own_markers)
        temp = item.name.split("_")
        print(temp)
        for i in temp:
            temp_mark = "pytest.mark." + i
            item.add_marker(eval(temp_mark))
        print(item.own_markers)

pytest总结_第28张图片

(2)pytest_addoption

添加用户自定义命令行参数:我实际用处是一个用例要不要检查mysql,要不要检查redis

# conftest.py中内容
# 内容说明: 添加了两个参数--cdb检查要不要和数据库对数
#                        --crd检查要不要和redis对数
# pytest_addoption方法只是让命令行认识了你的参数
# 下面的方法才能得到参数的值:request获取、pytestconfig获取(推荐)

import os
import pytest

def pytest_addoption(parser):
    """定义一些命令行参数"""
    parser.addoption("--check_db", action="store_const",
                     default=0,
                     const=1,
                     help="--check_db是否有mysql的操作")
    parser.addoption("--check_redis", action="store_const",
                     default=0,
                     const=1,
                     help="--check_redis是否有redis的操作")


@pytest.fixture(scope="session", autouse=True)
def checkout_db(request):
    """是否操作数据库--用例执行不方便执行mysql的时候就不执行了"""
    cdb_value = request.config.getoption("--check_db")
    print(type(cdb_value))
    os.environ['check_db'] = str(cdb_value)
    print('\n --check_db参数值:', cdb_value)
    return cdb_value


@pytest.fixture(scope="session", autouse=True)
def checkout_redis(request):
    """是否操作redis--用例执行不方便执行redis的时候就不执行了"""
    crd_value = request.config.getoption("--check_redis")
    os.environ['check_redis'] = str(crd_value)
    print('\n --check_redis参数值:', crd_value)
    return crd_value

测试文件

# _*_coding:utf-8_*_

import os


def test_addoption(checkout_db):
    res = checkout_db
    assert res == 1


def test_addoption1():
    res = os.environ['check_db']
    assert res == "1"

pytest总结_第29张图片

3、fixture使用

(1)作用范围

          scope参数指定范围:session>module>class>function

# -*- coding:utf-8 -*-

import pytest


@pytest.fixture(scope='session')
def fix_session():
    print('----- session create -----')
    driver = 'Chrome'
    yield driver
    print('----- session close -----')


@pytest.fixture(scope='class')
def fix_class():
    print('----- class create -----')
    driver1 = 'class'
    yield driver1
    print('----- class close -----')


@pytest.fixture(scope='module')
def fix_module():
    print('----- module create -----')
    driver2 = 'module'
    yield driver2
    print('----- module close -----')


@pytest.fixture(scope='function')
def fix_function():
    print('----- function create -----')
    driver3 = 'function'
    yield driver3
    print('----- function close -----')

测试文件1

def test_1(fix_session, fix_module, fix_class, fix_function):
    print("test_1")


def test_2(fix_session, fix_module, fix_class, fix_function):
    print("test_2")

测试文件2

class Test1:
    def test_1(fix_session, fix_module, fix_class, fix_function):
        print("test_1")

    def test_2(fix_session, fix_module, fix_class, fix_function):
        print("test_2")

测试 命令:pytest --setup-show  文件1 文件2

pytest总结_第30张图片

(2)fixture实现setup和tearDown

             yield前面是setup内容,后面是tearDown内容

# conftest内容

@pytest.fixture(params=["yes", "hello"])
def fixture_setup_teardown(request):
    print("setup")
    yield request.param
    print("teardown")

测试文件

def test_fixture(fixture_setup_teardown):
    print(fixture_setup_teardown)
    print("我才是主角")

 pytest总结_第31张图片

(3)fixture传递测试数据

         fixture函数return返回测试数据

# conftest文件内容

@pytest.fixture(scope="session", params=["opt1", "opt2"])
def optmod(request):
    return pytest.importorskip(request.param)

@pytest.fixture(scope="session")
def basemod(request):
    return pytest.importorskip("subprocess")

测试文件

def test_func1(basemod, optmod):
    print(basemod)
    print(optmod)

因为没有module是opt1和opt2和base,所以就会跳过

pytest总结_第32张图片

如果把opt1和opt2和base换为string,re,subprocess ,就会执行通过

pytest总结_第33张图片

(3)参数化fixture

这个参数indirect=True,一定不能少,要不就会直接把 fixture_param当成测试函数的一个参数来用,加上indirect=True这个参数,才会在fixture的函数中查找

@pytest.fixture(params=[1, 2, 3])
def fixture_param(request):
    print("我是fixture_param,这是第%s次打印" % request.param)
    return request.param


#覆盖了fixture_param的参数
@pytest.mark.parametrize("fixture_param", ["a", "b"], indirect=True) 
@pytest.mark.parametrize("a,b", [(1, 6), (2, 7), (3, 8), (4, 9)])
def test_fixture_param_and_parametrize(a, b, fixture_param):
    print("我是测试函数test_fixture_param_and_parametrize,参数a是%s,b是%s" % (a, b))


@pytest.mark.parametrize("a,b", [(1, 6), (2, 7), (3, 8), (4, 9)])
def test_fixture_param_and_parametrize1(a, b, fixture_param):
    print("我是测试函数test_fixture_param_and_parametrize,参数a是%s,b是%s" % (a, b))


if __name__ == '__main__':
    pytest.main(["-s", 'test_fixture_params.py'])

(4)使用方式

第一:调用方式

(1)作为参数传入测试函数

(2)@pytest.mark.usefixtures("fixture名字")方式传入

区别:标签方式没有办法获取fixture的返回值

           作为测试函数没有办法覆盖参数化

第二:执行顺序 

原则越靠近测试函数,越先执行

import pytest


@pytest.fixture(params=[1, 2])
def fixture_1(request):
    print("我是fixture_1_%s" % request.param)
    return request.param


@pytest.fixture(params=[4])
def fixture_2(request):
    print("我是fixture_2_%s" % request.param)
    return request.param


@pytest.mark.usefixtures("fixture_2")
@pytest.mark.usefixtures("fixture_1")
def test_fixture_1():
    print("我是测试函数1")


def test_fixture_2(fixture_1, fixture_2):
    print(fixture_1 + fixture_2)
    print("我是测试函数2")


@pytest.mark.usefixtures("fixture_1")
@pytest.mark.usefixtures("fixture_2")
def test_fixture_3():
    print("我是测试函数3")


def test_fixture_4(fixture_2, fixture_1):
    print(fixture_1 + fixture_2)
    print("我是测试函数4")


if __name__ == '__main__':
    pytest.main(["-s", 'test_fixture_params.py'])

pytest总结_第34张图片

第三:自动执行

简单点不细说

@pytest.fixture(scope="session", autouse=True)
def which_region(request):
    """区分region的命令行参数获取"""
    which_region = request.config.getoption("--region")
    os.environ['region'] = which_region
    print('\n --region参数值:', which_region)
    return which_region

4、用例参数化

(1)fixture传入测试数据(fixture中介绍过了)

(2)pytest.mark.parametrize()

对测试方法参数化

@pytest.mark.parametrize('a, b, c', [(1,2,3), (4,5,9), ('1', '2', '12')])
def test_add(a, b, c):
    print(f'\na,b,c的值:{a},{b},{c}')
    assert add(a, b) == c

对测试类参数化

@pytest.mark.parametrize('a, b, c', [(1,2,3), (4,5,9)])
class TestAdd():
    def test_add1(self, a, b, c):
        assert add(a, b) == c

    def test_add2(self, a, b, c):
        assert add(a, b) == c

(3)yaml文件驱动

安装yaml的包:PyYAML,举例我写的case把,不做demo了

pytest总结_第35张图片

(4)id和ids使用

from datetime import datetime, timedelta

import pytest


testdata = [
    (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
    (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]

# 没有ids的情况,运行展示不美观
@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
    diff = a - b
    assert diff == expected


@pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])
def test_timedistance_v1(a, b, expected):
    diff = a - b
    assert diff == expected


# ids也可以是一个构造id的函数,这里的val就是testdata
def idfn(val):
    if isinstance(val, (datetime,)):
        return val.strftime("%Y%m%d")


@pytest.mark.parametrize("a,b,expected", testdata, ids=idfn)
def test_timedistance_v2(a, b, expected):
    diff = a - b


@pytest.mark.parametrize(
    "a,b,expected",
    [
        pytest.param(
            datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id="forward"
        ),
        pytest.param(
            datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id="backward"
        ),
    ],
)
def test_timedistance_v3(a, b, expected):
    diff = a - b
    assert diff == expected
    assert diff == expected

pytest总结_第36张图片

(5)pytest.param使用

上面有试用了,需要提及的就是,可以对单个case做标记,我目前是用了标记,其他牛逼功能还没有使用

pytest总结_第37张图片

pytest总结_第38张图片

5、marks

感觉没啥说的呢,标记skip、skipif、xfail等

6、pytest处理mock

使用轻量的web服务框架,flask或者tornado

tornado可以同时监听多个端口,有队列处理等优点适合做为mock服务来使用,简单说下我如何使用的

# -*- coding:utf-8 -*-

import json
import os
import time
import traceback
from multiprocessing import Process

import tornado.ioloop
import tornado.web


class MockHandler(tornado.web.RequestHandler):

    def initialize(self, res_message, res_status):
        self.res_message = res_message
        self.res_status = res_status

    @tornado.gen.coroutine
    def get(self):
        uri = self.request.uri
        # 对getTaskCallBackInfo的mock做点特殊处理,在日志文件中找对应内容返回
        if uri.startswith("/getTaskCallBackInfo"):
            try:
                taskid = uri.split("taskid=")[1]
                # pathUtil的方法可以看做是拼接日志文件的路径方法
                with open(PathUtil.get_log_path(log_type="lixian", log_file=taskid), 'r') as f:
                    res = json.load(f)
                    self.write(res)
            except Exception as e:
                print(e)
                self.write({'errorCode': -1, 'message': '没有找到对应回调文件'})
        else:
            self.write(self.res_message)
        self.set_status(self.res_status)
        self.finish()

    @tornado.gen.coroutine
    @lixian_backlog
    @write_log
    def post(self):
        self.set_status(self.res_status)
        self.write(self.res_message)
        self.finish()


class MockDemo:
    """
    创建一个mock的需要知道的4个字段不要少
    :return:
    """
    # 如果没有初始化端口,uri等给一个默认的
    def __init__(self, **kwargs):

        if not kwargs.get("in_params"):
            in_params = [
                {
                    "port_param": 5053,
                    "mock_uris": [
                        {
                            "uri_param": "/",
                            "res_status": 200,
                            "res_message": '{"code":200}'
                        }
                    ]

                }
            ]
        else:
            in_params = kwargs.get("in_params")
        self.in_params = in_params
        self.address = "127.0.0.1"
        self.defult_port = 10081

    def start_server(self):
        for i in range(len(self.in_params)):
            # 拿不到port就是需要默认一个port
            try:
                port = int(self.in_params[i]["port_param"])
            except Exception as e:
                print(e)
                port = self.defult_port
            try:
                temp = self.in_params[i]["mock_uris"]
                mock_list = [(temp[index]["uri_param"], MockHandler,
                              {"res_message": temp[index]["res_message"], "res_status": temp[index]["res_status"]}) for
                             index in range(len(temp))]

                mock_application = tornado.web.Application(mock_list)
                mock_application.listen(port, self.address)
                print("tornado start:ip({}),port({}),params({})".format(self.address, port, temp))
            except Exception as e:
                info = traceback.print_exc()
                raise Exception("tornado web start failed: %s \n %s" % (e, info))
        i_o_instance = tornado.ioloop.IOLoop.instance()
        i_o_instance.start()

    def __call__(self):
        self.start_server()


def jifei_trade_mock():
    """业务需要的特定mock"""
    jifei_mock_param = [
        {
            "port_param": "8280",
            "mock_uris": [
                {
                    "uri_param": "/tradewer/saveOrdwwer",
                    "res_status": 200,
                    "res_message": {"status": 200, "message": None}
                },
                {
                    "uri_param": "/tradewer/notify",
                    "res_status": 200,
                    "res_message": {"status": 200, "message": "成功", "errorCode": None}
                },
            ]

        }
    ]

    return MockDemo(in_params=jifei_mock_param)



if __name__ == '__main__':
    # main里的调用方式就可以放在fixture中setup使用了
    hl = Process(target=jifei_trade_mock())
    hl.start()
    print('module name:', __name__)
    print('parent process:', os.getppid())
    print('process id:', os.getpid())
    for i in range(30):
        print("-----", time.time())
        time.sleep(2)
    hl.terminate()

7、日志处理

我使用了loguru

# -*- coding:utf-8 -*-
"""
Created on 2020/4/29
@author: hanfang1
description:日志基础类,日志用装饰器方式调用
"""
from loguru import logger

from common.path_util import PathUtil


class LogUtil:
    """
    日志处理
    """

    def __init__(self, log_type="", log_file="running", ts_flag=True):
        """
        log_type:根据不同的日志类型区分路径
        :param log_type:
        """
        # 这里是日志的输出路径方法,自行写一个就行了
        self.path = PathUtil.get_log_path(log_type=log_type, log_file=log_file, ts_flag=ts_flag)
        self.log_type = log_type

        logger.add(self.path,
                   filter=lambda x: log_type in x['message'],
                   format="{time:YYYY-MM-DD HH:mm:ss} | [" + log_type + "] | {level} | {message}",
                   rotation="500MB",
                   backtrace=True,
                   diagnose=True,
                   encoding="utf-8",
                   enqueue=True,
                   retention="10 days",
                   )

    def info(self, msg):
        return logger.info("[" + self.log_type + "] " + msg)

    def debug(self, msg):
        return logger.debug("[" + self.log_type + "] " + msg)

    def warning(self, msg):
        return logger.warning("[" + self.log_type + "] " + msg)

    def error(self, msg):
        return logger.error("[" + self.log_type + "] " + msg)



# 用这样方式保证不同的类型日志的单例模式
RunLog = LogUtil(log_type="run", log_file="running", ts_flag=True)
LixianLog = LogUtil(log_type="lixian", log_file="running", ts_flag=True)
ZhiboLog = LogUtil(log_type="zhibo", log_file="running", ts_flag=True)
MockLog = LogUtil(log_type="mock", log_file="running", ts_flag=True)

if __name__ == '__main__':
    RunLog.info("111111111111111")      # 日志只会打印到running.log日志中
    LixianLog.info("22222222222222")    # 日志只会打印到lixian.log日志中
    ZhiboLog.info("3333333333333333")
    MockLog.info("44444444444444444")

8、mysql和redis的封装

# -*- coding:utf-8 -*-


import pymysql.cursors
# conf就是mysql的配置项
from conf import mysql


def operator_status(func):
    """
    获取操作的状态,格式化
    """

    def gen_status(*args, **kwargs):
        error, result = None, None
        try:
            result = func(*args, **kwargs)
        except Exception as e:
            error = str(e)

        return {'result': result, 'error': error}

    return gen_status


class HandleMysql:
    def __init__(self):
        # 配置从config.py获取
        self.dbInfo = {
            "host": mysql.dbhost,
            "port": mysql.dbport,
            "user": mysql.dbuser,
            "password": mysql.dbpswd,
            "db": mysql.dbname
        }
        print("db read Success:%s" % self.dbInfo)

    def __enter__(self):
        try:
            self.__conn = pymysql.connect(**self.dbInfo)
            print("db connect Success")
            return self
        except Exception as e:
            print("db connect Failur: {}".format(e))

    def __exit__(self, exc_type, exc_val, exc_tb):
        """关闭数据库连接"""
        try:
            self.__conn.close()
            print("db close Success")
        except Exception as e:
            print("db close Failur: {}".format(e))

    @operator_status
    def insert(self, table, val_obj):
        """插入数据到数据表"""
        sql_top = 'INSERT INTO ' + table + ' ('
        sql_tail = ') VALUES ('
        try:
            for key, val in val_obj.items():
                sql_top += key + ','
                sql_tail += '"' + str(val) + '"' + ','
            sql = sql_top[:-1] + sql_tail[:-1] + ')'
            # print(sql)
            with self.__conn.cursor() as cursor:
                cursor.execute(sql)
            self.__conn.commit()
            return self.__conn.insert_id()
        except pymysql.Error:
            self.__conn.rollback()
            return False

    @operator_status
    def update(self, table, val_obj, range_str=""):
        """更新数据到数据表"""
        sql = 'UPDATE ' + table + ' SET '
        try:
            for key, val in val_obj.items():
                sql += key + '=' + '"' + val + '"' + ','
            if range_str:
                sql = sql[:-1] + ' WHERE ' + range_str
            with self.__conn.cursor() as cursor:
                cursor.execute(sql)
            self.__conn.commit()
            return cursor.rowcount
        except pymysql.Error:
            self.__conn.rollback()
            return False

    @operator_status
    def delete(self, table, range_str=""):
        """删除数据在数据表中"""
        if range_str:
            sql = 'DELETE FROM ' + table + ' WHERE ' + range_str
        else:
            sql = 'DELETE FROM ' + table
        try:
            with self.__conn.cursor() as cursor:
                cursor.execute(sql)
            self.__conn.commit()
            return cursor.rowcount
        except pymysql.Error:
            self.__conn.rollback()
            return False

    @operator_status
    def select_one(self, table, factor_str, field='*'):
        """查询唯一数据在数据表中"""
        sql = 'SELECT ' + field + ' FROM ' + table
        if factor_str:
            sql += ' WHERE ' + factor_str
        try:
            with self.__conn.cursor() as cursor:
                cursor.execute(sql)
            self.__conn.commit()
            return cursor.fetchall()[0]
        except pymysql.Error:
            return False

    @operator_status
    def select_more(self, table, range_str, field='*'):
        """查询多条数据在数据表中"""
        sql = 'SELECT ' + field + ' FROM ' + table
        if range_str:
            sql += ' WHERE ' + range_str
        try:
            with self.__conn.cursor() as cursor:
                cursor.execute(sql)
            self.__conn.commit()
            return cursor.fetchall()
        except pymysql.Error:
            return False

    @operator_status
    def exec_sql_str(self, sql_str):
        """拼接的sql字符串执行"""
        try:
            with self.__conn.cursor() as cursor:
                cursor.execute(sql_str)
            self.__conn.commit()
            return cursor.fetchall()
        except pymysql.Error:
            return False


if __name__ == "__main__":
    with HandleMysql() as db:
        print(db.exec_sql_str("select * from offline_preset  limit 1;"))
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==
# -*- coding:utf-8 -*-


import redis
# conf就是redis的配置项
from conf import redis_conf


def operator_status(func):
    """
    获取操作的状态,格式化
    """

    def gen_status(*args, **kwargs):
        error, result = None, None
        try:
            result = func(*args, **kwargs)
        except Exception as e:
            error = str(e)

        return {'result': result, 'error': error}

    return gen_status


class HandleRedis(object):
    def __init__(self):
        if not hasattr(HandleRedis, 'pool'):
            HandleRedis.create_pool()
        self._connection = redis.Redis(connection_pool=HandleRedis.pool)

    @staticmethod
    def create_pool():
        redis_info = {
            "host": redis_conf.redisHost,
            "port": redis_conf.redisPort,
            "password": redis_conf.redisPassword,
            "decode_responses": True,
            "retry_on_timeout": 3,
            "max_connections": 1024
        }
        print("redis read Success:%s" % redis_info)
        HandleRedis.pool = redis.ConnectionPool(**redis_info)

    @operator_status
    def set_data(self, key, value):
        """set data with (key, value)
        """
        return self._connection.set(key, value)

    @operator_status
    def get_data(self, key):
        """get data by key
        """
        return self._connection.get(key)

    @operator_status
    def del_data(self, key):
        """delete cache by key
        """
        return self._connection.delete(key)


if __name__ == '__main__':
    print(HandleRedis().set_data('Testkey', "Simple Test"))
    print(HandleRedis().get_data('Testkey'))
    print(HandleRedis().del_data('Testkey'))
    print(HandleRedis().get_data('Testkey'))

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

9、ssh操作远程机器

# -*- coding:utf-8 -*-
"""
description:ssh登录其他机器获取日志等操作
"""
import platform
if platform.system().lower() == 'linux':
    import paramiko



class Paramiko:
    def __init__(self, ip="127.0.0.1", user="root", password=None, port=22):
        self.ssh = paramiko.SSHClient()
        self.user = user
        self.ip = ip
        self.password = password
        self.port = port

    # 登录要测试的主机(linux主机)
    def ssh_login(self):
        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.ssh.connect(self.ip, self.port, self.user, self.password)

    def ssh_logout(self):
        self.ssh.close()

    # 执行linux命令
    def exec_commands(self, cmd):
        self.ssh_login()
        stdin, stdout, stderr = self.ssh.exec_command(cmd)
        results = stdout.read().decode("utf-8")
        self.ssh_logout()
        return results


    # 批量顺序执行
    def CMD(self, cmd_list=None):
        if cmd_list is None:
            cmd_list = []
        self.ssh_login()
        CmdDict = {}
        for c in cmd_list:
            CmdMes = self.exec_commands(c)
            print(CmdMes)
            CmdDict[c] = CmdMes
        self.ssh_logout()
        return CmdDict


if __name__ == "__main__":
    p = Paramiko("10.111.6.231", "hanfang1")
    res = p.exec_commands("grep -h 73006f75e886025cc351229227dfeb90 /home/hanfang1/log/money*")
    print(res)
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

10、分布式执行用例

重点:pytest-xdist没有内置的支持来确保会话范围的夹具仅执行一次

解决:锁方式实现(官网示例)pytest-xdist · PyPI

在conftest.py


@pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id):
    if worker_id == "master":
        LixianLog.info("master--start")
    else:
        root_tmp_dir = tmp_path_factory.getbasetemp().parent
        fn = root_tmp_dir / "data.json"
        with FileLock(str(fn) + ".lock"):
            if not fn.is_file():
                data = "slave--start"
                LixianLog.info(data)
                fn.write_text(data)
    yield
    if worker_id == "master":
        LixianLog.info("master--end")
    else:
        root_tmp_dir1 = tmp_path_factory.getbasetemp().parent
        fn1 = root_tmp_dir1 / "data1.json"
        with FileLock(str(fn1) + ".lock"):
            if not fn1.is_file():
                data1 = "slave--end"
                LixianLog.info(data1)
                fn1.write_text(data1)
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

测试方法:

# -*- coding:utf-8 -*-

import pytest
from common.log_util import RunLog


@pytest.mark.usefixtures("session_data")
@pytest.mark.parametrize("fff", range(10))
def test_2(fff):
    RunLog.info(str(fff))
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 pytest总结_第39张图片pytest总结_第40张图片

所以官网的方式只能解决前置的夹具啊

思路:修改一个进程为管理,控制整个运行过程(研究中)

11、用例重跑

https://cloud.tencent.com/developer/article/1189354
http://t.zoukankan.com/crystal1126-p-12580953.html
https://blog.csdn.net/weixin_53519100/article/details/113893416
https://www.cnblogs.com/crdym/p/14967837.html
pytest -s --html=D:\report2.html --lf  测试文件 重跑失败的用例

pip install pytest-rerunfailures
python -m pytest -v -n auto --reruns 2 test_offline_task.py --alluredir=./report

你可能感兴趣的:(python小知识,python)