安装pytest

pip install pytest

编写测试环境相关代码

在tests文件夹下, 创建一个名为conftest.py的文件

文如其名, conf-test, 测试的配置

所有的配置重点在于这个装饰器: @pytest.fixture(scope="session")
fixure有道的翻译为固定装置, 其实它就是每次测试的固定装置, 而且这也能理解为什么它会有scope(范围)这个参数
scope代表这个固定装置安装的范围或位置, 它有四个值:
"function" (default):作用于每个测试方法,每个 test 都运行一次
"class":作用于整个类,每个 class 的所有 test 只运行一次
"module":作用于整个模块,每个 module 的所有 test 只运行一次
"session:作用于整个 session(慎用),每个 session 只运行一次
"params":(list 类型)提供参数数据,供调用标记方法的函数使用
"autouse":是否自动运行,默认为 False 不运行,设置为 True 自动运行

import pytest
from main import app as myapp


@pytest.fixture()
def app():
    print(1111111111111111)
    myapp.testing = True

    # other setup can go here

    yield myapp

    # clean up / reset resources here


@pytest.fixture()
def client(app):
    print(22222222222222222222)
    return app.test_client()


@pytest.fixture()
def runner(app):
    print(333333333333333)
    return app.test_cli_runner()

编写测试用例

我们需要在tests文件夹下, 创建以test_开头的文件, 例如: test_api
①测试文件以 test_xx.py 命名需要以 test_开头(或_test 结尾)
②测试类以 Test开头,并且不能带有 init 方法
③测试函数或方法以 test 开头总体上分为三个阶段:
pytest会按照字符顺序执行测试用例
测试用例可以笼统归纳为两种: http请求和非http请求

Pytest 最常用的断言一般有以下五种:
assert xx:判断 xx 为真assert not xx:判断 xx 不为真assert a in b:判断 b 包含 a assert a == b:判断 a 等于 b assert a !=b:判断 a 不等于 b

import json


def test_logout(client):
    print(44444444444444)
    rv = client.get('/logout')

    assert rv.status_code == 200
    assert rv.json['code'] == 0

class TestAccess:
    def test_login_empty_username(self, client):
        headers = {
            'Content-Type': 'application/json'
        }

        data = {
            'password': 'test@123'
        }
        res = client.post('/login', headers=headers, data=json.dumps(data))
        assert res.status_code == 200, '网络状态码异常'
        assert res.json['code'] == -1, '业务状态码异常'

    def test_login_success(self, client):
        headers = {
            'Content-Type': 'application/json'
        }

        data = {
            'username': 'test',
            'password': 'test@123'
        }
        res = client.post('/login', headers=headers, data=json.dumps(data))
        assert res.status_code == 200, '网络状态码异常'
        assert res.json['code'] == 0, '业务状态码异常'

测试用例执行

单个文件执行
pytest 文件名 例: pytest ./test_access.py
flask使用pytest 单元测试-小白菜博客
多个文件执行
如果只输入 pytest,则会默认执行当前文件夹下所有以 test_开头(或_test 结尾)的文件。

pytest执行顺序

conftest中的app -> client -> 测试用例
image

初体验

项目目录结构

image

main.py

from flask import Flask, request

app = Flask(__name__)


@app.route('/login', methods=['POST'])
def login():
    body = request.json
    username = body.get('username')
    password = body.get('password')

    if not (username and password):
        return {'code': -1, 'msg': '用户名或密码为空'}

    print(1111, username, password)
    return {'code': 0, 'msg': '成功'}


@app.route('/logout', methods=['get'])
def logout():
    print(222, '退出登录')
    return {'code': 0, 'msg': '成功'}


if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)
	

conftest.py

import pytest
from main import app as myapp


@pytest.fixture()
def app():
    print(1111111111111111)
    myapp.testing = True

    # other setup can go here

    yield myapp

    # clean up / reset resources here


@pytest.fixture()
def client(app):
    print(22222222222222222222)
    return app.test_client()


@pytest.fixture()
def runner(app):
    print(333333333333333)
    return app.test_cli_runner()

test_access.py

import json


def test_logout(client):
    print(44444444444444)
    rv = client.get('/logout')

    assert rv.status_code == 200
    assert rv.json['code'] == 0

class Test_access:
    def test_login_empty_username(self, client):
        headers = {
            'Content-Type': 'application/json'
        }

        data = {
            'password': 'test@123'
        }
        res = client.post('/login', headers=headers, data=json.dumps(data))
        assert res.status_code == 200, '网络状态码异常'
        assert res.json['code'] == -1, '业务状态码异常'

    def test_login_success(self, client):
        headers = {
            'Content-Type': 'application/json'
        }

        data = {
            'username': 'test',
            'password': 'test@123'
        }
        res = client.post('/login', headers=headers, data=json.dumps(data))
        assert res.status_code == 200, '网络状态码异常'
        assert res.json['code'] == 0, '业务状态码异常'

【注意】在tests文件夹下需要有__init__.py文件,否在只用命令行执行pytest xxx相关命令的时候会提示"ModuleNotFoundError: No module named 'main'"找不到main模块。

参考文档: https://mp.weixin.qq.com/s?__biz=MzI3NDc4NTQ0Nw==&mid=2247526985&idx=1&sn=468201c0c30591a5ee28cf3676e1963a&chksm=eb0c9f81dc7b169753904d29b821b18ed595f210cfeeebea3692fff6d3359892b390467304cc&scene=27

测试覆盖率

安装pytest-cov

pip install pytest-cov

查看覆盖率命令

pytest --cov

查看覆盖率html报告

pytest --cov --cov-report=html

指定被测试代码

pytest --cov=apps
apps是项目模块名称

也可以指定具体的py模块名称

pytest --cov=apps.access
apps.access是指apps模块中的access.py文件

调整下上边的项目结构

image

image

image

将上边项目按找应用分文件夹重构,将之前main.py中的登录与登出相关的代码移动到apps/access.py中,并新加personal.py文件,写了个更改用户名的接口,测试用例内容没有变化

验证

执行pytest --cov
flask使用pytest 单元测试-小白菜博客
可以看到access.py中的覆盖率是100%,personal.py中的覆盖率是44%(总共9个有5个未被执行)。很奇怪,为什么测试用例没有变化,但是personal.py中会有被覆盖的情况???(下面会揭晓答案)

执行pytest --cov --cov-report=html
flask使用pytest 单元测试-小白菜博客
结果里没有任何信息,但是会当前目录生成一个htmlcov文件夹。打开htmlcov/index.html
flask使用pytest 单元测试-小白菜博客
可以看到这里的内容与上边执行pytest --cov的结果一模一样,但是,点击xx\apps\personal.py, 看到如下结果
image

看到了没,对于这个文件,总共有9行代码,有4行被执行了,5行没有被执行。(为什么4行被执行了,5行没被执行就不用解释了吧)
所以会有报告中44%的覆盖率了。这也就明白了,cov的覆盖率是怎么计算的。

cov的覆盖率是根据代码行数计算的,有多少行被执行了, 多少行未被执行

测试报告生成 allure

安装allure

Allure运行需要Java环境,Java安装并配置环境变量后,在命令窗口执行以下命令,更多信息可查阅官方文档
1.下载地址
https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/
image

下载版本-推荐下载最新的版本 windows下载zip的安装包,下载完之后直接解压

2.添加环境变量
flask使用pytest 单元测试-小白菜博客
image

image

3.检查是否安装成功
image

安装allure-pytest插件

pip install allure-pytest

生成allure报告

pytest [测试文件] -s -q --alluredir=./result #--alluredir指定测试报告路径

查看报告

方式一:直接打开默认浏览器展示报告
allure serve ./result

image

方式二:从结果生成报告

  • 生成报告
    allure generate ./result/ -o ./report/ --clean (覆盖路径加–clean)
  • 打开报告
    allure open -h 127.0.0.1 -p 8883 ./report/

参考文档:https://blog.csdn.net/u010698107/article/details/111416173

使用装饰器

因为我们在测试的时候每次都会需要请求网络,检查网络状态码和业务状态码,所以我们把这部分功能提取到装饰器中。

未使用装饰器代码

image

def test_b(client, base_url):
    name = '登出测试324'
    url = 'logout'
    _url = base_url + url
    res = client.get(_url)
    assert res.status_code == 200, f"[{name}]网络状态码异常"
    assert res.json['code'] == 0, f"[{name}]业务状态码异常"

base_url是一个fixture
image

使用装饰器提取公共部分

image

import json


def do_assert(method='get', headers=None, data=None, name=None, url=None):
    def wrap(func):
        def inner(client, base_url, *args, **kwargs):
            _url = base_url + url
            if method == 'get':
                res = client.get(_url)
            elif method == 'post':
                res = client.post(_url, headers=headers, data=data)
            else:
                assert 0, 'method 不合法'

            assert res.status_code == 200, f"[{name}]网络状态码异常"
            assert res.json['code'] == 0, f"[{name}]业务状态码异常"
            func(client, base_url, *args, **kwargs)

        return inner

    return wrap


@do_assert(name='登出测试', url='logout')
def test_logout(*args, **kwargs): ...


@do_assert(name='登录测试',
           url='login',
           method='post',
           headers={
               'Content-Type': 'application/json'
           },
           data=json.dumps({
               'username': 'test',
               'password': 'test@123'
           })
           )
def test_login(*args, **kwargs):
    print('我是函数内执行的哦')

与上边未使用装饰器的代码功能等价,如果有其他的操作,比如要查询数据库之类的,可以在函数内实现,在使用的之后装饰器会执行函数中的内容。

参数化用例,比上边装饰器更高级好用

@pytest.mark.parametrize(argnames, argvalues)

argnames:参数化变量名,可以是string(逗号分割) 、list 和 tuple类型

  • @pytest.mark.parametrize("a, b", [(1,2),(3,4)])

  • @pytest.mark.parametrize(["a","b"], [(1,2),(3, 4)])

  • @pytest.mark.parametrize(("a", "b"), [(1,2),(3,4)])

flask使用pytest 单元测试-小白菜博客
可以看到,上边是有三条用例的
image

从csv文件读取数据
image

@pytest.mark.parametrize('a,b', pd.read_csv('test_collections.csv').values)
def test_a(a, b):
    print(111111111, a, b)
    assert a == b

执行结果
image

参数来源可以通过文件,变量各种形式。参见文档
https://blog.51cto.com/u_15441270/4719922

好用????????????????????????????????????????????????????