☰
Python使用pytest高效编写和管理单元测试的完整指南
2026/10/2 2:30:51 网站建设 项目流程

这里有一份完整的方法, 就是怎么去高效率地把单元测试写出来, 并且还怎么去管理这些测试的。

更新时间:2026年01月30日 15:31:31 作者:小庄-办公

在现在的主流开发过程里, 做测试这件事情的重要性那是非常明显的, 对于那些刚开始学习或者刚进入这个行业的人来说, 最能够直接感受到的好处就是把需要写的代码数量给降下来了, 接下来我就按照顺序给大家把这具体该怎么操作的事情讲个明白详细说说。

为什么 是 开发者的必选工具

在开发生态中, 测试的重要性是不言而喻的。虽然标准库自带了模块, 但是, 凭借它那种简洁的语法, 强大的插件生态系统, 以及对那些复杂测试场景的天然支持, 它已经成为了事实上的行业标准。

对于刚刚入门的新手来讲, 一个让他们一眼就能看明白的好处, 就是他们需要写的代码变少了。这个工具并不要求你的测试类要去继承一个指定的基类, 同时也不会强制你非要使用某种特定的断定方法。

你可以直接去调用那些标准的语句, 再配上它那种智能的断言重写机制, 这样的话, 一旦测试出现了失败的情况, 你就能得到非常多、非常详细的错误消息提醒。

案例对比:

假设我们现在要测试的是那种非常简单的, 用来做加法运算的这个函数 add(a, b)。

写法:

import unittest

from my_module import add

class TestAdd(unittest.TestCase):

def test_add_positive_numbers(self):

self.assertEqual(add(1, 2), 3)

写法:

from my_module import add

def test_add_positive_numbers():

assert add(1, 2) == 3

大家都能看得出来, 那种写法的操作方式比较顺手, 而且非常贴近我们平时去编写一般函数的习惯。正因为它的参与门槛很低, 所以团队的成员们可以更快地开始进行测试代码的编写工作, 这样大家在心里就不用为了维护和修改测试代码而耗费太多精力。

另外, 系统本身具备自动识别的功能, 只要看到是以 test_ 打头的函数或者文件, 它就能找出来, 根本不需要大家再去手动地把这些测试用例组装成一个套组, 这种强调约定好过手动配置的思路能够大幅度提高开发的效率。

必须要弄清楚的要点包括参数化设置, 以及如何进行数据的模拟测试。

为了能够把威力充分地释放出来, 光是去写那些基础的断言操作是不行的。大家必须要把它的三个主要功能全都掌握清楚, 这三个部分分别是参数化、也就是测试夹具的部分, 还有模拟对象这个叫法的Mock功能。

1. 所谓参数化测试, 指的是这样一种技术手段。

在做实际开发工作的时候, 大家通常都得用不同组的输入数据, 去对一个函数进行功能验证。要是给每组单独的数据都去写一个测试函数的话, 这代码看起来就会太啰嗦, 显得特别冗余。

好在提供了名为 @.mark. 的这样一个装饰器, 这个装饰器的作用, 是让咱们能够通过一种相对比较简洁的方式, 来把多组数据全都传递进去处理。

import pytest

from my_module import is_prime

@pytest.mark.parametrize("number, expected", [

(2, True),

(3, True),

(4, False),

(10, False),

(17, True)

])

def test_is_prime(number, expected):

assert is_prime(number) == expected

采用此种操作模式, 假使存在一组具体的数据在进行的测试环节里出现了故障现象, 那么系统将会清楚地指出具体是哪一组的输入信息成为了引发该问题的原因所在, 与此同时, 针对其余各项数据所对应的测试任务也照样会被完整执行下去。

2. :管理测试资源的神器

测试往往并不是孤立进行的, 它们有时候还需要连接数据库、读取配置文件或者启动一个临时的Web服务。这种机制是专门用来应对这些在测试之前需要做的准备工作, 以及测试之后必须进行的清理工作的。

这个功能是通过利用 @. 这个装饰器来进行定义的。具体来说, 我们是把函数的名字当作参数传递给了测试函数。这样的做法一方面实现了代码的复用, 另一方面还很好地支持了依赖注入。这里说的依赖注入, 意思就是允许一个东西去依赖于另一个东西。

示例:模拟数据库连接

import pytest

@pytest.fixture

def db_connection():

# Setup: 建立连接

print("\n连接数据库...")

connection = {"status": "connected"}

yield connection

# Teardown: 关闭连接

print("\n断开数据库...")

def test_db_query(db_connection):

# 测试函数直接使用 fixture 返回的对象

assert db_connection["status"] == "connected"

这种 yield 机制确保了, 不管测试的结果是顺利通过还是出现失败情况, 后面用于进行清理操作的那些代码段都会被去执行一遍, 这种做法对于来确保测试环境能够保持整洁状态来说是非常关键的一个环节。

3. 把外面的那些依靠给隔离开, 这就是Mock要做的事情。

单元测试里头那个最根本的原则就是隔离。要是你这代码要依托于第三方 API、外面那些服务或者是还没弄好的模块, 那你直接去调用这些依赖根本就行不通, 为啥呢? 因为那样速度太慢, 而且还特别难掌控。不过, 借助内置支持\.mock 库这一点, 让我们就能轻松把这些给换掉。

打个例子来说, 要是去测试一个执行HTTP请求的那个函数呢, 咱们压根子就不需要去真个儿地发送那个请求, 反而是把那个.get这个方法给Mock掉, 让它是返回咱们提前就设定好的响应数据。这个做法不光儿是把测试的速度提得飞快, 而且还是避免了因为网络的那些波动情况所带来的那些不稳定的影响因素。

在进阶实战的这个部分里面, 我们需要把相关的技术或者工具结合起来去进行数据库集成测试这样一个事情。

尽管单元测试侧重于隔离, 但是, 在诸多情况下, 比如在后端开发环节中, 咱们需要实施集成测试, 去检验代码同数据库之间的交流逻辑, 借助某些驱动工具的话, 便能够高效率地达成此项目标。

这里存在着一个经典的场景问题, 这个问题主要讨论的是在进行测试的时候, 应该如何确保对数据库的操作是安全的。具体的做法是要避免生产出任何被污染的数据情况出现, 同时也要防止开发环境中原本存在的数据库状态遭到破坏作用。

该策略的核心要点在于, 通过实施与管理相关事务的回滚操作, 从而达到预期目的。

我们可以去编写一个东西, 这个事物会开启一个数据库事务, 并且执行测试的动作, 然后在测试完成那个时间点上把这个事务给回滚掉, 这样一来, 测试过程中产生的那些数据变更内容就不会真正地去写入到数据库里面去了。

代码示例:

import pytest

import psycopg2

# 数据库连接配置 (建议从环境变量读取)

DB_CONFIG = {

"dbname": "test_db",

"user": "postgres",

"password": "password",

"host": "localhost"

}

@pytest.fixture(scope="function")

def db_cursor():

"""

创建数据库连接和游标,并在测试结束后回滚。

scope="function" 表示每个测试函数都会运行一次此 fixture。

"""

conn = psycopg2.connect(**DB_CONFIG)

cursor = conn.cursor()

# 开始事务

cursor.execute("BEGIN")

yield cursor

# 测试结束,回滚事务

cursor.execute("ROLLBACK")

cursor.close()

conn.close()

def test_insert_user(db_cursor):

"""

测试插入用户逻辑。

即使插入成功,由于事务回滚,数据库中不会留下这条记录。

"""

# 假设有一个 users 表

db_cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Alice",

db_cursor.execute("SELECT count(*) FROM users WHERE name = %s", ("Alice",))

count = db_cursor.fetchone()[0]

assert count == 1

def test_query_user(db_cursor):

"""

验证上一个测试没有影响当前环境。

"""

db_cursor.execute("SELECT count(*) FROM users WHERE name = %s", ("Alice",))

count = db_cursor.fetchone()[0]

# 因为上一个测试回滚了,这里应该查不到 Alice

assert count == 0

分析:

我们是借助了这个方式, 这才把测试数据被污染掉的这个问题给非常圆满地解决掉。之所以能这样做, 是因为那个叫scope=""的东西起作用了。它能让每一个单独的测试例子在开始时, 全都拥有一个全新干净的与事务相关的环境状态。大家都认为这种操作模式是在写那些很结实的数据库集成测试文件时, 最好的使用方法。

在扩展功能方面, 主要是对插件生态进行建设的同时, 还开展了针对UDP协议的测试。

它的强大之处, 不光是在它自己本身上面, 更重要的一点是它有一个规模特别大的插件生态系统。不管你的需求有多少数派、有多冷门, 你基本上都能够找到相匹配的插件可以用来解决。具体的例子如下。

关于这一种比较特殊的情况, 咱们需要去弄明白到底应该怎么才对 udp 协议代码开展各种测试操作?

在网络编程这个领域里面,UDP是一种无连接的协议, 所以对于它进行测试, 要比对TCP进行测试更加微妙。尽管用户自己提供的标签当中包含了“UDP”这个字样, 但是, 在相关的环境之中去测试那些使用了UDP的代码, 通常是需要涉及到网络库(比方说某些特定的库)的模拟工作的。

假设咱们现在有个UDP监听器, 咱们得去验证一下它能不能把收到的数据包给整明白。要是直接在测试代码里把端口绑上, 那很容易就会出现端口冲突或者权限不够的麻烦情况。一般最靠谱的做法是做个Mock。

import socket

import pytest

from my_module import start_udp_listener

def test_udp_listener_logic(monkeypatch):

# 模拟接收到的数据

mock_data = b"Hello UDP"

# 用来存储发送回客户端的数据

sent_data = []

class MockSocket:

def __init__(self, family, type_):

pass

def bind(self, address):

pass

def recvfrom(self, bufsize):

# 模拟接收到数据和发送方地址

return (mock_data, ("127.0.0.1", 8080))

def sendto(self, data, addr):

sent_data.append(data)

def close(self):

pass

# 使用 monkeypatch 替换 socket.socket

monkeypatch.setattr(socket, "socket", MockSocket)

# 执行被测代码 (注意:这里需要确保代码能被调用并阻塞,或者在单独线程运行)

# 实际测试中,通常会将逻辑提取出来单独测试,或者使用超时机制

# 这里仅作演示:假设 start_udp_listener 内部逻辑处理了 recvfrom 并调用 sendto

# 模拟监听器的一次循环

sock = MockSocket(socket.AF_INET, socket.SOCK_DGRAM)

data, addr = sock.recvfrom(1024)

# 假设处理逻辑是回显大写

processed_data = data.upper()

sock.sendto(processed_data, addr)

assert sent_data[0] == b"HELLO UDP"

这个例子展示了怎样利用相关的东西, 把网络层隔离开去, 从而专心致力于测试业务逻辑, 所谓的业务逻辑就是指对数据进行处理的环节以及生成响应的那个环节。

总结

这玩意儿, 压根儿就不是一个简简单单拿来跑测试用的工具。它是一套, 完完整整, 彻彻底底的, 测试解决方案。

不管你是去写那种比较简单的代码脚本, 还是说要维护那些规模非常大的、用在企业里的软件应用, 只要你能把这个知识点深入地去掌握清楚, 那么毫无疑问的就会让代码整体的质量得到非常明显的提升, 同时也会让开发人员在编写代码的时候信心变得非常大。

到这个时候, 这篇介绍如何使用高效方式编写与管理单元测试的完整指南的内容就已经讲述完毕了。如果你希望了解更多的关于单元测试相关的内容, 你可以去搜索一下脚本之家之前发布过的文章, 或者也可以继续浏览下方推荐的其他相关主题的文章。希望在将来能够得到大家的多多支持与帮助, 谢谢大家关注脚本之家。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询