基于pytest的自动化测试实践

基于pytest的自动化测试实践

[TOC]

1. 什么是pytest?

一款强大python的测试框架.

具有如下特点:

  • 适用范围广: 可以用于所有级别和类型的软件测试.
    • 开发工程师: 用于单元测试(UnitTest)
    • QA: 代码质量检测
    • 测试工程师: 接口测试, 功能测试, UI测试
    • TDD: 测试驱动开发
    • 开发测试平台
  • 简单
# test_*.py
def test_plus():
        assert (1 + 1) == 2
  • 强大: 可编写插件进行功能扩展,并内置了很多优秀插件.
    • pytest-bdd, 行为驱动测试
    • pytest-html,测试结果生成html的报告
    • pytest-xdist,并行执行测试
    • pytest-cov,测试覆盖率检查
    • ...

2. 编写第一个pytest脚本: Hello, world

1. 项目初始化:

//Python3.8
> mkdir bn-test  // 创建项目
> cd bn-test // 进入项目根目录
> python3 -m venv venv  // 创建python虚拟环境
> source venv/bin/activate  // 激活虚拟环境
> pip install pytest  // 安装pytest框架

2. 创建测试脚本: test_hello_world.py

# test_hello_world.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

3. 运行pytest命令

> pytest
============================= test session starts ==============================
platform darwin -- Python 3.8.6, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/anna/projects/bn-test
collected 1 item                                                               

test_hello_world.py F                                                    [100%]

=================================== FAILURES ===================================
_________________________________ test_answer __________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_hello_world.py:8: AssertionError
=========================== short test summary info ============================
FAILED test_hello_world.py::test_answer - assert 4 == 5
============================== 1 failed in 0.06s ===============================

3. 案例一: 算法的自动化测试

3.1 案例描述

某公司对历史客户数据进行分析, 发现客户的消费力基于某因素具有明显规律性特征. 现开发了一套能预测新客户消费潜力的算法, 需要对算法的有效性进行测试.

3.2 项目准备

安装依赖:

> pip install numpy
> pip install matplotlib
> pip install sklearn

目录结构:

|--perdict_alg
     |--__init__.py
     |--conftest.py
     |--alg.py
     |--data.py
     |--test_alg.py

3.3 历史数据

模拟产生历史数据. y表示消费力, x表示影响消费力高低的因素.

# data.py
import numpy as np
def get_history_data():
    x = np.linspace(-3, 3, 100)
    y = 2 * x + 1
    x = x + np.random.rand(100)
    return x, y
history_data = get_history_data()

查看上面的数据到底是什么样子的?

# data.py
def show():
    import matplotlib.pyplot as plt
    plt.scatter(history_data[0], history_data[1])
    plt.show()
    
if __name__ == '__main__':
    show()
image-20210110002659697.png

3.4 预测算法

# alg.py
from sklearn import linear_model
from predict_alg.data import history_data

model = linear_model.LinearRegression()  # 使用线性回归模型
model.fit(history_data[0].reshape(-1, 1), history_data[1])  # 训练模型

def predict(x_):
    y_ = model.predict([[x_], ])  # 预测
    return y_[0]

3.5 测试脚本

测试用例: 从历史数据抽取全部或部分数据, 调用预测算法得到预测值, 跟对应的真实值进行比较,在允许的误差范围内说明预测成功,否则失败.

新建pytest的配置脚本

# conftest.py
from predict_alg.data import history_data

def pytest_generate_tests(metafunc):
    ids, test_data = [], []
    if "parameters" in metafunc.fixturenames:
        for i, x_ in enumerate(history_data[0]):
            ids.append(i)
            test_data.append({'id': i, 'x': x_, 'y': history_data[1][i]})
        metafunc.parametrize("parameters", test_data, ids=ids, scope="function")

编写测试用例

# test_alg.py
from predict_alg.alg import predict
    
def test_predict(parameters):
    y_ = predict(parameters['x'])
    y = parameters['y']
    assert abs(y_ - y) < 1

3.6 测试结果

(venv) anna@chenxuehuideMacBook-Pro predict_alg % pytest
=================================== test session starts ====================================
platform darwin -- Python 3.8.6, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/anna/projects/bn-test/predict_alg
collected 100 items                                                                        

test_alg.py ..........F.................................................F........... [ 72%]
...........................F                                                         [100%]

========================================= FAILURES =========================================
_____________________________________ test_predict[10] _____________________________________

parameters = {'id': 10, 'x': -1.4091625376598214, 'y': -3.787878787878788}

    def test_predict(parameters):
        y_ = predict(parameters['x'])
        y = parameters['y']
>       assert abs(y_ - y) < 1
E       assert 1.008378271252539 < 1
E        +  where 1.008378271252539 = abs((-2.779500516626249 - -3.787878787878788))

test_alg.py:10: AssertionError
_____________________________________ test_predict[60] _____________________________________

parameters = {'id': 60, 'x': 0.641274440559893, 'y': 2.2727272727272734}

    def test_predict(parameters):
        y_ = predict(parameters['x'])
        y = parameters['y']
>       assert abs(y_ - y) < 1
E       assert 1.0356455432403757 < 1
E        +  where 1.0356455432403757 = abs((1.2370817294868977 - 2.2727272727272734))

test_alg.py:10: AssertionError
_____________________________________ test_predict[99] _____________________________________

parameters = {'id': 99, 'x': 3.0154483831587497, 'y': 7.0}

    def test_predict(parameters):
        y_ = predict(parameters['x'])
        y = parameters['y']
>       assert abs(y_ - y) < 1
E       assert 1.1121706453377698 < 1
E        +  where 1.1121706453377698 = abs((5.88782935466223 - 7.0))

test_alg.py:10: AssertionError
================================= short test summary info ==================================
FAILED test_alg.py::test_predict[10] - assert 1.008378271252539 < 1
FAILED test_alg.py::test_predict[60] - assert 1.0356455432403757 < 1
FAILED test_alg.py::test_predict[99] - assert 1.1121706453377698 < 1
=============================== 3 failed, 97 passed in 1.89s ===============================

结果分析

这个案例中唯一需要关心的只有成功率指标.当前数据在将来也有变成历史数据, 样本容量对预测模型的影响具有不确定性.就是说预测算法随着时间的推移, 随时可能失效.这就需要算法自动化测试脚本的去发现失效的临界点.

4. 案例二: 接口的自动化测试

4.1 案例描述

测试豆瓣电影列表的API

image-20210109004721700.png
  • URL: https://movie.douban.com/j/search_subjects

  • Method: get

  • Params:

    • type: 类型
    • tag:标签
    • page_limit:每页记录数
    • page_start: 第几页
  • Example:

    https://movie.douban.com/j/search_subjects?type=movie&tag=热门&page_limit=50&page_start=0
    
  • Response:

    {"subjects":[{"rate":"8.9","cover_x":6611,"title":"心灵奇旅","url":"https:\/\/movie.douban.com\/subject\/24733428\/","playable":false,"cover":"https://img9.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2626308994.webp","id":"24733428","cover_y":9435,"is_new":false},{"rate":"6.3","cover_x":2764,"title":"神奇女侠1984","url":"https:\/\/movie.douban.com\/subject\/27073752\/","playable":false,"cover":"https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2626959989.webp","id":"27073752","cover_y":4096,"is_new":false}]}
    

4.2 项目准备

安装依赖:

> pip install pytest-html
> pip install requests

目录结构:

|--douban_api
     |--__init__.py
     |--test_movie_api.py

4.3 测试用例

  1. 测试每页记录数参数是否有效: 输入page_limit=2, 预期结果记录数等于2
  2. 测试每页参数是否有效: 输入page_start=1的结果和page_start=2的结果 是否一样
  3. 测试标签等于豆瓣高分时,结果是否按高分排序

4.4 测试脚本

# test_movie_api.py
import requests

class TestMovieApi(object):
    url = 'https://movie.douban.com/j/search_subjects'
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
    }
    
    def test_page_limit_valid(self):
        result = requests.get(self.url, params={
            'type': 'movie',
            'tag': '热门',
            'page_limit': 2,
            'page_start': 0,
        }, headers=self.headers).json()
        assert len(result['subjects']) == 2

    def test_page_start_valid(self):
        result = requests.get(self.url, params={
            'type': 'movie',
            'tag': '热门',
            'page_limit': 1,
            'page_start': 0,
        }, headers=self.headers).json()

        result2 = requests.get(self.url, params={
            'type': 'movie',
            'tag': '热门',
            'page_limit': 1,
            'page_start': 1,
        }, headers=self.headers).json()
        
        assert result['subjects'][0]['title'] != result2['subjects'][0]['title']
        
    def test_score_sort(self):
        result = requests.get(self.url, headers=self.headers, params={
            'type': 'movie',
            'tag': '豆瓣高分',
            'page_limit': 3,
            'page_start': 0,
        })
        
        assert result['subjects'][0]['rate'] >= result['subjects'][1]['rate']
        assert result['subjects'][1]['rate'] >= result['subjects'][2]['rate']

4.5 测试结果

> pytest --html=report.html
image-20210111002451346.png

5. 案例三: UI的自动化测试

5.1 案例描述

测试CSDN的登陆功能

  1. 进入csdn首页
image-20210117113016922.png
  1. 点击网页头部的“登陆/注册”链接, 进入登陆页面
image-20210117115810288.png
  1. 点击“账号密码登录”,进入输入用户名密码界面

    image-20210117120022815.png
  1. 输入账号( ****** )和密码( ****** ),点击“登陆”按钮
image-20210117120200335.png
  1. 验证登陆是否成功

    登陆后, 进入https://i.csdn.net/#/user-center/profile, 验证dom( div.person-name )元素的text内容是否时用户名

    <div data-v-df75d7f8="" class="person-name">wanghao524151</div>
    
image-20210117120446339.png

5.2 项目准备

安装依赖:

> brew install allure
> pip install allure-pytest
> pip install playwright
> python -m playwright install

目录结构:

|--csdn_ui
     |-- img
     |--__init__.py
     |--test_login.py

5.3 测试脚本

# test_login.py

import allure
from playwright import sync_playwright


@allure.feature('测试登陆功能')
class TestLogin(object):
    browser = None
    
    def setup_method(self, method):
        print('测试登陆开始')
        pw = sync_playwright().start()
        self.pw = pw
        self.browser = pw.chromium.launch(headless=False, slowMo=100)
            
    def teardown_method(self, method):
        self.browser.close()
        self.pw.stop()
        print('测试登陆结束')

    @allure.step('1、进入csdn首页')
    def step_1(self, page):
        page.goto('https://www.csdn.net/')
        page.waitForLoadState()
        page.screenshot(path='img/step_1.png')
        allure.attach.file('img/step_1.png', '步骤1截图', attachment_type=allure.attachment_type.PNG)

    @allure.step('2、点击登陆链接')
    def step_2(self, page):
        page.click('text="登录/注册"')
        page.waitForLoadState()
        page.screenshot(path='img/step_2.png')
        allure.attach.file('img/step_2.png', '步骤2截图', attachment_type=allure.attachment_type.PNG)

    @allure.step('3、点击账号密码登陆')
    def step_3(self, page):
        page.click('text="账号密码登录"')
        page.screenshot(path='img/step_3.png')
        allure.attach.file('img/step_3.png', '步骤3截图', attachment_type=allure.attachment_type.PNG)

    @allure.step('4、输入账号密码,点击登陆按钮')
    def step_4(self, page):
        page.type("input[name=all]", "******")
        page.type("input[name=pwd]", "******")
        page.screenshot(path='img/step_4.png')
        allure.attach.file('img/step_4.png', '步骤4截图', attachment_type=allure.attachment_type.PNG)
        page.click("button.btn.btn-primary")
        page.waitForNavigation()

    @allure.step('5、验证登陆是否成功')
    def step_5(self, page):
        page.goto('https://i.csdn.net/#/user-center/profile')
        page.waitForLoadState('networkidle')
        page.screenshot(path='img/step_5.png')
        allure.attach.file('img/step_5.png', '步骤5截图', attachment_type=allure.attachment_type.PNG)
        username = page.innerText('div.person-name')
        assert username == 'wanghao524151'
    
    @allure.story('测试账号密码登陆')
    @allure.title('用例#1')
    def test_login(self):
        page = self.browser.newPage()
        self.step_1(page)
        self.step_2(page)
        self.step_3(page)
        self.step_4(page)
        self.step_5(page)

执行测试脚本:

> pytest --alluredir=/tmp/my_allure_results

启动allure:

> allure serve /tmp/my_allure_results

5.4 测试结果

image-20210117162231760.png
image-20210117162016333.png

转载请注明来源: https://www.jianshu.com/p/1ada6bd33f79
作者: Anna2Tim

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • ``` import requests import json if __name__=='__main__': ...
    anhus阅读 157评论 0 0
  • 程序实现目标 用户输入电影类型,如【热门 最新 经典 华语】 可获得此分类排名 Top 100 电影信息(电影名 ...
    SavageT阅读 1,499评论 1 0
  • Python (计算机编程语言) Python由荷兰数学和计算机科学研究学会的Guido van Rossum 于...
    申炎阅读 359评论 0 0
  • Windows快捷键大全 标准计算机键盘共104键,除了26个字母键、符号键、数字键外,其余按键功能如下: Win...
    欣静园阅读 673评论 0 0
  • 引言:CMS缺陷: 单核、双核情况下,效率低。 foreground 模式(并发失败导致)下垃圾回收占用时间较长,...
    竹blue阅读 304评论 0 1