首页 > 其他分享 >Pytest:一个卓有成效的测试工具

Pytest:一个卓有成效的测试工具

时间:2023-07-31 15:31:56浏览次数:48  
标签:fixture py 卓有成效 pytest test Pytest 测试 测试工具 def


大家都知道,目前最流行的Python单元测试框架有三种,分别是unittest, nose和pytest。其中unittest是Python自带的测试框架,但问题是比较老了,赶不上时代发展了(哈哈哈);nose2定位是带插件的unittest,实则对unittest的扩展。长远来看,pytest属于潜力股。

通过官网介绍的特点和使用经验,可以将pytest优点总结如下:

1)支持用简单的assert语句实现丰富的断言,无需复杂的self.assert*函数

2)可以自动识别测试模块和测试函数

3)兼容unittest和nose测试集

4)支持参数化

5)支持测试用例的skip和xfail处理

6)可以很好的和jenkins集成

7)支持丰富的插件,例如报告插件pytest-html、allure-pytest、失败重试插件pytest-rerunfailures、

8)活跃的社区,遇到的问题可以高效解决。

简化样板代码

大多数功能测试遵循 Arrange-Act-Assert 模型:

  1. 设置测试前置条件
  2. 调用函数来执行测试
  3. 断言执行结果

测试框架通常会挂接到测试的断言中,以便它们可以在断言失败时提供信息。unittest,例如,提供了许多开箱即用的断言方法,但是不友好的地方是,unittest编写的用例即使是一小部分测试也需要相当数量的样板代码。

下面写一个unittest测试用例并断言在项目中正常工作。

# test_with_unittest.py
from unittest import TestCase
class TryTesting(TestCase):
 def test_always_passes(self):
 self.assertTrue(True)
 def test_always_fails(self):
 self.assertTrue(False)

然后,你可以使用命令行运行这些测试:

(venv) $ python -m unittest discover
F.
======================================================================
FAIL: test_always_fails (test_with_unittest.TryTesting)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "...\effective-python-testing-with-pytest\test_with_unittest.py",
 line 10, in test_always_fails
 self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------

Ran 2 tests in 0.006s

FAILED (failures=1)

一个测试通过,一个测试失败。OK,下面我们总结一下一个完整的测试需要写多少代码:

  1. 导入类unittest引入TestCase
  2. 创建TryTesting,一个子类TestCase
  3. TryTesting为每个测试写一个方法
  4. 使用self.assert*方法进行断言

这是要编写的大量代码的,所以在开发测试用例时会一遍又一遍地编写相同的样板代码。而pytest则不同,它允许你直接使用普通函数和python assert关键字来简化样板代码达到相同的目的:

# test_with_pytest.py
def test_always_passes():
 assert True
def test_always_fails():
 assert False

就是如此简单。你需要做的就是写一个带有test_前缀的函数,使用assert关键字断言期望是True/False,然后执行测试即可。

Pytest不仅消除了很多样板代码,而且还提供了更加详细和易于阅读的输出。

更好的测试输出

在项目根目录文件夹下使用pytest命令运行所有测试用例:

(venv) $ pytest
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items
test_with_pytest.py .F [ 50%]
test_with_unittest.py F. [100%]
================================== FAILURES ===================================
______________________________ test_always_fails ______________________________
 def test_always_fails():
> assert False
E assert False
test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________
self = <test_with_unittest.TryTesting testMethod=test_always_fails>
 def test_always_fails(self):
> self.assertTrue(False)
E AssertionError: False is not true
test_with_unittest.py:10: AssertionError
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...
========================= 2 failed, 2 passed in 0.20s =========================

不同于unittest,pytest的测试结果展示的更详细:

  1. 系统状态,包括 Python的版本,pytest以及你安装的任何插件
  2. 测试目录
  3. runner发现的测试数量

这些内容显示在输出的第一部分:

============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items

输出使用以下的语法指示每个测试的状态:

  • 点 (.)表示测试通过。
  • AnF表示测试失败。
  • AnE表示测试引发了意外异常。

特殊字符显示在用例名称后,右侧显示测试套件的整体进度:

test_with_pytest.py .F [ 50%]

test_with_unittest.py F. [100%]

对于失败的测试,报告会详细说明失败情况。

================================== FAILURES ===================================
______________________________ test_always_fails ______________________________
 def test_always_fails():
> assert False
E assert False
test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________
self = <test_with_unittest.TryTesting testMethod=test_always_fails>
 def test_always_fails(self):
> self.assertTrue(False)
E AssertionError: False is not true
test_with_unittest.py:10: AssertionError

下面这个额外的输出在调试时非常有用。最后报告给出了测试用例执行的整体状态报告:

=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...
========================= 2 failed, 2 passed in 0.20s =========================

与 unittest 相比,pytest输出的信息量和可读性要高得多。

强大的assert

assert关键字很强大,支持丰富多样的断言形式。以下是一些断言示例,你可以了解支持的断言类型:

# test_assert_examples.py
def test_uppercase():
 assert "loud noises".upper() == "LOUD NOISES"
def test_reversed():
 assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1]
def test_some_primes():
 assert 37 in {
 num
 for num in range(2, 50)
 if not any(num % div == 0 for div in range(2, num))
 }

更容易管理前置参数和依赖

众所周知,测试用例通常需要依赖准备数据或者其他服务作为前置条件。

使用unittest,你可以将这些依赖项提取到方法中.setUp(),.tearDown()以便类中的每个测试都可以使用它们。使用这些特殊方法很好,但是随着你的测试类变得越来越大,你可能会不经意地使测试的依赖完全隐式。换句话说,通过孤立地查看众多测试中的一个,你可能不会立即看出它依赖于其他东西。

随着时间的推移,隐式依赖关系会导致代码变得复杂,你必须展开这些代码才能理解测试用例。事实上,测试应该有助于使你的代码更易于理解。如果测试本身很难理解,那么就有问题了!

pytest采取不同的方法。由于fixture的可用性,它会引导你进行显式的依赖声明,这些声明可以重用。fixture 是可以为测试用例创建数据、测试mock或初始化系统状态的函数。任何想要使用fixture的测试都必须显式地使用这个fixture函数作为测试函数的参数,所以依赖关系总是在前面声明:

# fixture_demo.py
import pytest
@pytest.fixture
def example_fixture():
 return 1
def test_with_fixture(example_fixture):
 assert example_fixture == 1

查看测试函数,你可以立即看出它依赖于一个fixture,而无需检查整个文件的fixture定义。

易于过滤测试

随着测试套件的增多,你可能会有只想对某个功能运行一些测试并保存整个套件以备后用的需求。pytest提供了一些方法来实现这一点:

  • 基于名称的过滤:你可以限制pytest只运行那些完全限定名称与特定表达式匹配的测试。你可以使用-k参数执行此操作。
  • 目录范围:默认情况下,pytest将仅运行当前目录中/下的测试用例。
  • 测试分类:pytest可以包含或排除你定义的特定类别的测试,可以使用-m参数执行此操作。

测试分类是一个非常强大的工具。pytest使你能够为你喜欢的任何测试创建标签或自定义标签。一个测试可能有多个标签,你可以使用它们来精细化控制要运行的测试。

丰富的插件

pytest生态是开源的,pytest用户开发了一个丰富的有用插件生态系统。

fixture

Pytest fixture是一种为测试提供数据、测试mock的方法。每个依赖于fixture的测试都必须显式地接受fixture作为参数。

何时创建fixture

假设你正在编写一个函数format_data_for_display()来处理 API 接口返回的数据。输入数据是一个人员列表,每个人都有给定的姓名、姓氏和职位。该函数应输出一个字符串列表,其中包括每个人的全名、冒号和title:

# format_data.py
def format_data_for_display(people):
 ... # Implement this!

践行TDD模式,你需要为其编写测试用例。

# test_format_data.py
def test_format_data_for_display():
 people = [
 {
 "given_name": "Alfonsa",
 "family_name": "Ruiz",
 "title": "Senior Software Engineer",
 },
 {
 "given_name": "Sayid",
 "family_name": "Khan",
 "title": "Project Manager",
 },
 ]
 assert format_data_for_display(people) == [
 "Alfonsa Ruiz: Senior Software Engineer",
 "Sayid Khan: Project Manager",
 ]

在开发测试用例时,你可能需要开发另一个函数来将数据转换为逗号分隔值以便在Excel中使用:

# format_data.py
def format_data_for_display(people):
 ... # Implement this!
def format_data_for_excel(people):
 ... # Implement this!

你的TODO清单变长了!TDD 的优势之一是它可以帮助你规划未来的工作。format_data_for_excel()函数的测试看起来与format_data_for_display()函数非常相似:

# test_format_data.py
def test_format_data_for_display():
 # ...
def test_format_data_for_excel():
 people = [
 {
 "given_name": "Alfonsa",
 "family_name": "Ruiz",
 "title": "Senior Software Engineer",
 },
 {
 "given_name": "Sayid",
 "family_name": "Khan",
 "title": "Project Manager",
 },
 ]
 assert format_data_for_excel(people) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""

值得注意的是,这两个测试都必须重复people变量的定义,而这需要相当多的代码。

如果你正在编写多个测试,这些测试都使用相同的底层测试数据,那么使用fixture你可以将重复的数据拉入一个装饰到函数中,使用@pytest.fixture表示该函数是一个pytest fixture:

# test_format_data.py
import pytest
@pytest.fixture
def example_people_data():
 return [
 {
 "given_name": "Alfonsa",
 "family_name": "Ruiz",
 "title": "Senior Software Engineer",
 },
 {
 "given_name": "Sayid",
 "family_name": "Khan",
 "title": "Project Manager",
 },
 ]
# ...

你可以通过将函数引用作为参数添加到测试中来使用fixture,这样可以使用 fixture函数的返回值作为 fixture 函数的名称:

# test_format_data.py
# ...
def test_format_data_for_display(example_people_data):
 assert format_data_for_display(example_people_data) == [
 "Alfonsa Ruiz: Senior Software Engineer",
 "Sayid Khan: Project Manager",
 ]
def test_format_data_for_excel(example_people_data):
 assert format_data_for_excel(example_people_data) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,
"""

每个测试的代码量明显更短,但仍然有一条清晰的路径返回它所依赖的数据。

什么时候避免使用fixture

fixture非常适合提取你在多个测试中使用的数据或对象。但是,对于需要数据有变化的测试,fixture并不总是那么好。在你的测试套件中乱用fixture,可能会导致情况更糟。

与大多数抽象一样,需要一些实践和思考才能找到合适的fixture使用场景。

尽管如此,fixture 很可能是你的测试套件不可或缺的一部分。随着项目范围的扩大,测试规模的挑战开始出现。任何类型的工具面临的挑战之一是它如何应对大规模使用,pytest具有一系列有用的功能,可以帮助你管理用例增长带来的复杂性。

如何规模化使用fixture

当你从测试中提取更多的fixture时,你可能会发现一些fixture可以从进一步的抽象中受益。在 中pytest,fixture是模块化的。模块化意味着 fixture 可以导入,可以导入其他模块,它们可以依赖和导入其他 fixture。所有这些都允许你为你的用例编写合适的fixture抽象。

例如,你可能会发现两个单独文件或模块中的fixture共享一个共同的依赖项。在这种情况下,你可以将fixture从测试模块移动到更通用的fixture相关模块中,然后就可以将它们导入任何需要它们的测试模块中。

如果你想让一个fixture在你的整个项目中可用而不必导入它,可以使用特殊配置模块conftest.py文件。pytest在每个目录中查找一个conftest.py模块。如果你将通用fixture添加到conftest.py模块中,那么你将能够在整个模块的父目录和任何子目录中使用该fixture,而无需导入它。conftest.py是放置高频率使用fixture的好地方。

conftest.py另一个有用的地方是它可以保护对资源的访问。想象一下,你已经为处理API 调用的代码编写了一个测试套件,你希望确保测试套件不会进行任何真正的网络调用,即使有人不小心编写了这样做的测试。

pytest提供一个monkeypatch fixture来替换值和行为,你可以使用它来产生很好的效果:

# conftest.py
import pytest
import requests
@pytest.fixture(autouse=True)
def disable_network_calls(monkeypatch):
 def stunted_get():
 raise RuntimeError("Network access not allowed during testing!")
 monkeypatch.setattr(requests, "get", lambda *args, **kwargs: stunted_get())

通过放置disable_network_calls()和conftest.py添加autouse=True的选项,你可以确保在整个套件的每个测试中都将禁用网络调用。任何执行代码调用的测试requests.get()都会引发一个RuntimeError指示将发生意外网络调用的错误。

marks:分类测试

在任何大型测试套件中,当你尝试快速迭代新功能时,最好避免运行所有测试,做好精细化测试。除了pytest在当前工作目录中运行所有测试的过滤功能之外,你还可以利用mark。

pytest使你能够为测试定义类别,并提供在运行套件时包含或排除类别的选项。你可以使用任意数量的类别标记测试。

标记测试对于按子系统或依赖项对测试进行分类很有用。例如,如果你的某些测试需要访问数据库,那么你可以@pytest.mark.database_access为它们创建一个标记。

当需要运行测试时,你仍然可以使用命令默认运行它们pytest。如果你只想运行那些需要访问数据库的测试,那么你可以使用pytest -m database_access. 要运行除需要访问数据库的测试之外的所有测试,你可以使用pytest -m "not database_access". 你甚至可以使用autousefixture来限制对那些标有 的测试的数据库访问database_access。

pytest-django插件提供了一个django_db标记。没有此标记的任何尝试访问数据库的测试都将失败。尝试访问数据库的第一个测试将触发 Django 测试数据库的创建。

pytest提供开箱即用的一些标记:

  • skip无条件跳过测试。
  • skipif如果传递给它的表达式的计算结果为True,则跳过测试。
  • xfail表示测试会失败,因此如果测试确实失败,整个套件仍会导致通过状态。
  • parametrize使用不同的值作为参数创建测试的多个变体。

Parametrization:组合测试

上文提到了如何使用fixture提取公共依赖项来减少代码重复。当你进行多个输入和预期输出略有不同的测试时,fixture就没有那么有用了。在这些情况下,你可以使用参数化为单个测试定义指定参数。

假设你编写了一个函数来判断一个字符串是否为回文。一组初始测试代码如下所示:

def test_is_palindrome_empty_string():
 assert is_palindrome("")
def test_is_palindrome_single_character():
 assert is_palindrome("a")
def test_is_palindrome_mixed_casing():
 assert is_palindrome("Bob")
def test_is_palindrome_with_spaces():
 assert is_palindrome("Never odd or even")
def test_is_palindrome_with_punctuation():
 assert is_palindrome("Do geese see God?")
def test_is_palindrome_not_palindrome():
 assert not is_palindrome("abc")
def test_is_palindrome_not_quite():
 assert not is_palindrome("abab")

除了最后两个之外,所有这些测试都具有相同的样板:

def test_is_palindrome_<in some situation>():
 assert is_palindrome("<some string>")

乍看很像样板文件。我们可以使用@pytest.mark.parametrize()减少测试代码:

第一个参数parametrize()是以逗号分隔的参数名称字符串。第二个参数是表示参数值的元组或单个值的列表。你可以将参数化进一步处理,将所有测试合并为一个:

@pytest.mark.parametrize("maybe_palindrome, expected_result", [
 ("", True),
 ("a", True),
 ("Bob", True),
 ("Never odd or even", True),
 ("Do geese see God?", True),
 ("abc", False),
 ("abab", False),
])
def test_is_palindrome(maybe_palindrome, expected_result):
 assert is_palindrome(maybe_palindrome) == expected_result

尽管这缩短了你的代码,但重要的是要注意,在这种情况下,你实际上丢失了原始函数的一些更具描述性的特性。确保你没有将测试套件参数化到难以理解的地步之前,可以使用参数化将测试数据与测试行为分开,以便清楚测试用例在测试什么,同时也使不同的测试用例更易于阅读和维护。

durations

命令行使用--durations,pytest在测试结果中会包含持续时间报告。--durations需要一个整数值n,并将报告最慢的n测试:

(venv) $ pytest --durations=5
...
============================= slowest 5 durations =============================
3.03s call test_code.py::test_request_read_timeout
1.07s call test_code.py::test_request_connection_timeout
0.57s call test_code.py::test_database_read
(2 durations < 0.005s hidden. Use -vv to show these durations.)
=========================== short test summary info ===========================
...

显示在持续时间报告中的每个测试占用的总测试时间高于平均水平。

注:某些测试也可能有不可见的设置开销。例如django_db将触发 Django 测试数据库的创建。durations报告反映了在触发数据库创建的测试中设置数据库所花费的时间。

标签:fixture,py,卓有成效,pytest,test,Pytest,测试,测试工具,def
From: https://blog.51cto.com/u_15503184/6909092

相关文章

  • 国产化的接口测试、接口自动化测试工具Apipost的介绍及使用
    Apipost介绍:Apipost是API文档、API调试、APIMock、API自动化测试一体化的研发协作赋能平台,它的定位Postman+Swagger+Mock+JMeter。Apipost是接口管理、开发、测试全流程集成工具,能支撑整个研发技术团队同平台工作,主要使用者为前端开发、后端开发、测试人员。Apipost优......
  • 国产化的接口测试、接口自动化测试工具Apipost的介绍及使用
    Apipost介绍:Apipost是API文档、API调试、APIMock、API自动化测试一体化的研发协作赋能平台,它的定位Postman+Swagger+Mock+JMeter。Apipost是接口管理、开发、测试全流程集成工具,能支撑整个研发技术团队同平台工作,主要使用者为前端开发、后端开发、测试人员。Apipo......
  • 开源jvm性能基准测试工具之renaissance
    JVM标准的性能测试工具是SPECjbb2015,SPECjbb2015是SPEC组织的一个用于评估服务器端Java应用性能的基准测试程序,其官方主页为 https://www.spec.org/jbb2015 。在其之前还有SPECjbb2013、SPECjbb2005等版本。该基准测试主要测试Java虚拟机(JVM)、JIT编译器、垃圾回收、Java线程......
  • Pytest收集用例
    有时候我们需要收集一下当前的测试用例,获的所有测试用例的列表,在使用pytest的测试框架里,我们可以使用pytest<path>--collect-only-q来仅收集(不运行)用例。但是使用这个命令用例列表只会显示在命令行中,如何在代码中使用并得到这个用例列表呢?除了使用os.popen()或subprocess从......
  • 网络性能测试工具iperf3
    1.创建两个容器#创建第一个容器dockerrun-d--namecontainer1ubuntu:latestsleepinfinity#创建第二个容器dockerrun-d--namecontainer2ubuntu:latestsleepinfinity2.查找第一个容器的ip地址dockerinspectcontainer13.进入第一个容器,并安装iperf3。......
  • 基准测试工具 --- BenchmarkDotNet
    介绍今天介绍一个非常强大的基于.Net的基准测试工具BenchmarkDotNet。BenchmarkDotNet已经被14300多个项目采用,包括非常多的知名开源项目,例如dotnet/performance(.Net所有运行时的基准测试项目)dotnet/runtime(.Net运行时库),Roslyn(c#和VisualBasic编译器),Mono、ASP.NET......
  • 基准测试工具 --- BenchmarkDotNet
    介绍今天介绍一个非常强大的基于.Net的基准测试工具BenchmarkDotNet。BenchmarkDotNet已经被14300多个项目采用,包括非常多的知名开源项目,例如dotnet/performance(.Net所有运行时的基准测试项目)dotnet/runtime(.Net运行时库),Roslyn(c#和VisualBasic编译器),Mono、ASP.NET......
  • pytest xfail 已知错误 失败也不报错
    [email protected]('a,b,sum',[#参数化出来3组数据(1,2,3),(2,3,5),(3,-19,-16)])deftest_add(a,b,sum):asserta+b==sum#如果名称等于main输出pytest.main([文档,sv参数])@pytest.mark.xfail#已知的错误失败也不报错d......
  • pytest 参数化
    [email protected]('a,b,sum',[#参数化出来3组数据(1,2,3),(2,3,5),(3,-19,-16)])deftest_add(a,b,sum):asserta+b==sum#如果名称等于main输出pytest.main([文档,sv参数])if__name__=='__main__':pytest.main([__......
  • 想快速上手性能测试,测试工具不会用?试试RunnerGo!简单易上手
    当前,性能测试已经是一名软件测试工程师必须要了解,甚至熟练使用的一项技能了,在工作时可能每次发版都要跑一遍性能,跑一遍自动化。性能测试入门容易,深入则需要太多的知识量,今天这篇文章给大家带来:怎么入门性能测试,怎么样去深入性能测试。突然让做性能测试怎么办?日常工作中,项目改造或项......