Menu Close

Python PyTest

什么是PyTest?

PyTest是一个测试框架,允许用户使用Python编程语言编写测试代码。它可以帮助您为数据库,API或UI编写简单且可扩展的测试用例。PyTest主要用于编写API测试。它有助于编写从简单的单元测试到复杂的功能测试的测试。

为什么要使用PyTest?

pytest的一些优点是

  • 由于语法简单易上手,因此非常容易上手。
  • 并行运行测试。
  • 运行特定的测试
  • 自动检测测试
  • skip测试

如何安装PyTest

以下是有关如何安装PyTest的过程:

1)安装pytest

pip install pytest

安装完成后,查看版本

py.test -V

output:

pytest 6.2.2

第一个PyTest

现在,我们将通过一个简单的示例学习如何使用Pytest。

创建一个名为test_01.py的文件

1
pytest根据命名约定收集测试。默认情况下,任何包含测试的文件都必须以开头命名,test_文件中任何应被视为测试的函数也必须以test_开头

示例代码:

import pytest
def test_file1_method1():
   x=5
   y=6
   assert x+1 == y,"test_failed_line_5"
   assert x == y,"test_failed_line_6"
def test_file1_method2():
   x=5
   y=6
   assert x == y-1,"test_failed_line_10"

使用pytest命令运行测试
%title插图%num
红色的 “F”  表示失败

绿色的   “.”   表示成功。

在本PyTest教程的下一步,我们将学习PyTest中的断言。

PyTest中的断言

Pytest断言是返回True或False状态的检查。在Python Pytest中,如果断言在测试方法中失败,则该方法的执行在那里停止。该测试方法中的其余代码不会执行,Pytest断言将继续使用下一个测试方法。

Pytest断言示例:

import  pytest
assert "hello" == "hello"
assert 4==3
assert True
assert False

output:

=============================================== test session starts=======================================================
platform win32 -- Python 3.8.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\PycharmProjects\firstProject
collected 0 items / 1 error

================================================== ERRORS==========================================================
__________________________________ ERROR collecting test_01.py________________________________________
assert 4==3
E assert 4 == 3
========================================== short test summary info=====================================================
ERROR test_01.py - assert 4 == 3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!
===================================================== 1 error in 0.11s==========================================================

 

PyTest如何识别测试文件和测试方法

默认情况下,仅pytest标识开头的文件名TEST_或结束_test作为测试文件。不过,我们可以明确提及其他文件名(稍后说明)。Pytest要求测试方法名称以“ test ”开头。即使我们明确要求运行这些方法,所有其他方法名称也将被忽略。

示例

test_login.py - valid
login_test.py - valid
testlogin.py -invalid
logintest.py -invalid

注意:是的,我们可以明确要求pytest选择test_login.py和login_test.py

示例

def test_file1_method1(): - valid
def testfile1_method1(): - valid
def file1_method1(): - invalid

注意:即使我们明确提到file1_method1,pytest也不会运行此方法。

从一个特定的文件和多个文件运行多个测试

如果需要测试当前文件夹和子文件夹中的所有文件,我们只需要进入当前文件夹的控制台运行pytest命令。

例如在C:\Users\Administrator\PycharmProjects\firstProject路径下执行命令:

py.test

该命令会将该文件夹中所有以test_开头的文件名和以_test结尾的文件名以及该文件夹下的子文件夹的测试程序运行

如果你只需要测试特定文件,我们可以使用py.test <filename>,例如:

py.test test_sample1.py

Pytest并行测试

通常,一个测试套件将具有多个测试文件和数百种测试方法,这将花费相当多的时间来执行。Pytest允许我们并行运行测试。

为此,我们需要先通过运行以下命令来安装pytest-xdist

pip install pytest-xdist

 

您现在可以通过以下方式运行测试

py.test -n 4

-n <num>通过使用多个工作程序来运行测试。在上面的命令中,将同时运行4个测试程序。

Pytest Fixture

当我们想在每个测试方法之前运行一些代码时,会使用fixture。因此,我们需要定义fixture,而不是在每个测试中都重复相同的代码。通常,fixture用于初始化数据库连接,传递基数等

通过用标记一个方法,将方法标记为Pytest fixture

@pytest.fixture

示例:

import pytest
@pytest.fixture
def supply_AA_BB_CC():
   aa=25
   bb =35
   cc=45
   return [aa,bb,cc]

def test_comparewithAA(supply_AA_BB_CC):
   zz=35
   assert supply_AA_BB_CC[0]==zz,"line_11_failed"

def test_comparewithBB(supply_AA_BB_CC):
   zz=35
   assert supply_AA_BB_CC[1]==zz,"line_15_failed"

def test_comparewithCC(supply_AA_BB_CC):
   zz=35
   assert supply_AA_BB_CC[2]==zz,"line_19_failed"

说明

  • 我们有一个fixture  supply_AA_BB_CC。此方法将返回3个值的list。
  • 我们有3种测试方法与每个值进行比较。

每个测试函数都有一个输入自变量,其名称与可用的夹具匹配。然后Pytest调用相应的fixture方法,返回的值将存储在输入参数中,此处为列表[25,35,45]。现在,这些列表项已在测试方法中用于比较。

现在运行测试并查看结果

 py.test test_01.py
================================================ test session starts ================================================
platform win32 -- Python 3.8.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\PycharmProjects\firstProject
plugins: forked-1.3.0, xdist-2.2.1
collected 3 items

test_01.py F.F [100%]

===================================================== FAILURES ======================================================
________________________________________________ test_comparewithAA _________________________________________________

supply_AA_BB_CC = [25, 35, 45]

def test_comparewithAA(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[0]==zz,"line_11_failed"
E AssertionError: line_11_failed
E assert 25 == 35

test_01.py:11: AssertionError
________________________________________________ test_comparewithCC _________________________________________________

supply_AA_BB_CC = [25, 35, 45]

def test_comparewithCC(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[2]==zz,"line_19_failed"
E AssertionError: line_19_failed
E assert 45 == 35

test_01.py:19: AssertionError
============================================== short test summary info ==============================================
FAILED test_01.py::test_comparewithAA - AssertionError: line_11_failed
FAILED test_01.py::test_comparewithCC - AssertionError: line_19_failed
============================================ 2 failed, 1 passed in 0.10s ============================================

 

由于zz = BB = 35,所以已通过test test_comparewithBB,其余2个测试均失败。

Fixture方法仅在定义的测试文件中具有作用域。如果尝试访问其他测试文件中的fixture,则会收到一条错误消息,提示未在其他文件中的测试方法中找到fixture“ supply_AA_BB_CC”

要对多个测试文件使用相同的fixture,我们将在名为conftest.py的文件中创建fixture。

让我们通过下面的PyTest示例来了解这一点。使用以下代码创建3个文件conftest.py,test_basic_fixture.py,test_basic_fixture2.py

conftest.py

import pytest
@pytest.fixture
def supply_AA_BB_CC():
    aa = 25
    bb = 35
    cc = 45
    return [aa, bb, cc]

test_basic_fixture.py

import pytest
def test_comparewithAA(supply_AA_BB_CC):
    zz = 35
    assert supply_AA_BB_CC[0] == zz, "aa and zz comparison failed"
def test_comparewithBB(supply_AA_BB_CC):
    zz = 35
    assert supply_AA_BB_CC[1] == zz, "bb and zz comparison failed"
def test_comparewithCC(supply_AA_BB_CC):
    zz = 35
    assert supply_AA_BB_CC[2] == zz, "cc and zz comparison failed"

test_basic_fixture2.py

import pytest


def test_comparewithAA_file2(supply_AA_BB_CC):
    zz = 25
    assert supply_AA_BB_CC[0] == zz, "aa and zz comparison failed"


def test_comparewithBB_file2(supply_AA_BB_CC):
    zz = 25
    assert supply_AA_BB_CC[1] == zz, "bb and zz comparison failed"


def test_comparewithCC_file2(supply_AA_BB_CC):
    zz = 25
    assert supply_AA_BB_CC[2] == zz, "cc and zz comparison failed"

pytest将首先在测试文件中查找fixture,如果找不到,它将在conftest.py中查找

通过py.test -k test_comparewith -v运行测试以得到如下结果

test_basic_fixture.py::test_comparewithAA FAILED [ 16%]
test_basic_fixture.py::test_comparewithBB PASSED [ 33%]
test_basic_fixture.py::test_comparewithCC FAILED [ 50%]
test_basic_fixture2.py::test_comparewithAA_file2 PASSED [ 66%]
test_basic_fixture2.py::test_comparewithBB_file2 FAILED [ 83%]
test_basic_fixture2.py::test_comparewithCC_file2 FAILED [100%]

Pytest参数化测试

参数化测试的目的是针对多组参数运行测试。我们可以通过@pytest.mark.parametrize

我们将在下面的PyTest示例中看到这一点。在这里,我们将3个参数传递给测试方法。此测试方法将添加前2个参数,并将其与第3个参数进行比较。

使用以下代码创建测试文件test_addition.py

import pytest
@pytest.mark.parametrize("input1, input2, output",[(5,5,10),(3,5,12)])
def test_add(input1, input2, output):
	assert input1+input2 == output,"failed"

这里的测试方法接受3个参数-输入1,输入2,输出。它将输入1和输入2相加,并与输出进行比较。

测试结果:

================================================ test session starts ================================================
platform win32 -- Python 3.8.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\PycharmProjects\firstProject
plugins: forked-1.3.0, xdist-2.2.1
collected 2 items

test_addition.py .F [100%]

===================================================== FAILURES ======================================================
_________________________________________________ test_add[3-5-12] __________________________________________________

input1 = 3, input2 = 5, output = 12

@pytest.mark.parametrize("input1, input2, output", [(5, 5, 10), (3, 5, 12)])
def test_add(input1, input2, output):
> assert input1 + input2 == output, "failed"
E AssertionError: failed
E assert (3 + 5) == 12

test_addition.py:6: AssertionError
============================================== short test summary info ==============================================
FAILED test_addition.py::test_add[3-5-12] - AssertionError: failed
============================================ 1 failed, 1 passed in 0.09s ============================================

Pytest Xfail / Skip Tests

在某些情况下,我们不想执行测试,或者在特定时间内测试案例不相关。在这种情况下,我们可以选择Xfail测试或跳过测试

将执行xfailed测试,但不会将其计为部分失败或通过的测试。如果该测试失败,将不会显示任何回溯。我们可以使用以下方法进行xfail测试

@pytest.mark.xfail

跳过测试意味着将不会执行测试。我们可以使用跳过测试

@pytest.mark.skip

使用以下代码编辑test_addition.py

import pytest


@pytest.mark.skip
def test_add_1():
    assert 100 + 200 == 400, "failed"


@pytest.mark.skip
def test_add_2():
    assert 100 + 200 == 300, "failed"


@pytest.mark.xfail
def test_add_3():
    assert 15 + 13 == 28, "failed"


@pytest.mark.xfail
def test_add_4():
    assert 15 + 13 == 100, "failed"


def test_add_5():
    assert 3 + 2 == 5, "failed"


def test_add_6():
    assert 3 + 2 == 6, "failed"

说明:

  • test_add_1和test_add_2被跳过,将不会执行。
  • test_add_3和test_add_4失败。这些测试将被执行,并将成为xfailed(测试失败)或xpassed(测试通过)测试的一部分。不会有任何失败的回溯。
  • 当test_add_5通过时,将执行test_add_5和test_add_6,并且test_add_6将报告失败并进行追溯

通过py.test test_addition.py -v执行测试并查看结果

================================================ test session starts ================================================
platform win32 -- Python 3.8.0, pytest-6.2.2, py-1.10.0, pluggy-0.13.1 -- c:\users\administrator\appdata\local\program
s\python\python38\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\Administrator\PycharmProjects\firstProject
plugins: forked-1.3.0, xdist-2.2.1
collected 6 items

test_addition.py::test_add_1 SKIPPED (unconditional skip) [ 16%]
test_addition.py::test_add_2 SKIPPED (unconditional skip) [ 33%]
test_addition.py::test_add_3 XPASS [ 50%]
test_addition.py::test_add_4 XFAIL [ 66%]
test_addition.py::test_add_5 PASSED [ 83%]
test_addition.py::test_add_6 FAILED [100%]

===================================================== FAILURES ======================================================
____________________________________________________ test_add_6 _____________________________________________________

def test_add_6():
> assert 3 + 2 == 6, "failed"
E AssertionError: failed
E assert 5 == 6
E +5
E -6

test_addition.py:29: AssertionError
============================================== short test summary info ==============================================
FAILED test_addition.py::test_add_6 - AssertionError: failed
=========================== 1 failed, 1 passed, 2 skipped, 1 xfailed, 1 xpassed in 0.07s ============================

Pytest框架测试API

现在,我们将创建一个小的pytest框架来测试API。这里使用的API是https://reqres.in/中的免费API 。

在这里,我们将为编写一些测试

  • 查询用户
  • 用户登录

使用给定的代码创建以下文件

conftest.py  可以为所有测试方法提供基本网址

import pytest
@pytest.fixture
def supply_url():
	return "https://reqres.in/api"

test_list_user.py –包含列出有效和无效用户的测试方法

  • test_list_valid_user测试有效的用户访存并验证响应
  • test_list_invaliduser测试无效的用户访存并验证响应

测试之前确保安装了reuqests模块【pip install requests】

import pytest
import requests
import json


@pytest.mark.parametrize("userid, firstname", [(1, "George"), (2, "Janet")])
def test_list_valid_user(supply_url, userid, firstname):
    url = supply_url + "/users/" + str(userid)
    resp = requests.get(url)
    j = json.loads(resp.text)
    assert resp.status_code == 200, resp.text
    assert j['data']['id'] == userid, resp.text
    assert j['data']['first_name'] == firstname, resp.text


def test_list_invaliduser(supply_url):
    url = supply_url + "/users/50"
    resp = requests.get(url)
    assert resp.status_code == 404, resp.text

test_login_user.py –包含用于测试登录功能的测试方法。

  • test_login_valid使用电子邮件和密码测试有效的登录尝试
  • test_login_no_password在不通过密码的情况下测试无效的登录尝试
  • test_login_no_email在不传递电子邮件的情况下测试了无效的登录尝试。
import pytest
import requests
import json


def test_login_valid(supply_url):
    url = supply_url + "/login/"
    data = {'email': 'test@test.com', 'password': 'something'}
    resp = requests.post(url, data=data)
    j = json.loads(resp.text)
    assert resp.status_code == 200, resp.text
    assert j['token'] == "QpwL5tke4Pnpja7X", resp.text


def test_login_no_password(supply_url):
    url = supply_url + "/login/"
    data = {'email': 'test@test.com'}
    resp = requests.post(url, data=data)
    j = json.loads(resp.text)
    assert resp.status_code == 400, resp.text
    assert j['error'] == "Missing password", resp.text


def test_login_no_email(supply_url):
    url = supply_url + "/login/"
    data = {}
    resp = requests.post(url, data=data)
    j = json.loads(resp.text)
    assert resp.status_code == 400, resp.text
    assert j['error'] == "Missing email or username", resp.text

使用py.test -v运行测试

结果:

test_list_user.py::test_list_valid_user[1-George] PASSED [ 16%]
test_list_user.py::test_list_valid_user[2-Janet] PASSED [ 33%]
test_list_user.py::test_list_invaliduser PASSED [ 50%]
test_login_user.py::test_login_valid FAILED [ 66%]
test_login_user.py::test_login_no_password PASSED [ 83%]
test_login_user.py::test_login_no_email PASSED [100%]

您还可以尝试修改相应参数查看输出结果;

 

python系列教程目录

Posted in Python

发表评论

相关链接