测试初学者指南:单元、冒烟、验收
简介
单元测试
冒烟测试
验收测试
免责声明
本文表达的想法和观点仅代表作者本人,并不一定反映作者任职或曾任职的公司的观点。该公司不认可或承担本文内容的责任。任何对特定产品、服务或公司的引用均不代表这些公司的认可或推荐。作者对本文信息的准确性和完整性负全部责任。这些公司对本文内容中的任何错误、遗漏或不准确之处概不负责。
简介
目前,我在 Meta 工作,我的团队负责实现和支持后端服务集成测试的基础设施。由于工作性质特殊,我计划撰写一系列关于后端服务测试的文章,这些文章尽可能通俗易懂,并包含示例和说明,希望对所有人,尤其是初学者有所帮助。
在加入 Meta 之前,我曾在一家动态油藏模拟公司和一家大型科技银行工作。由于这些公司产品的重要性,进行适当且可靠的测试至关重要。这些测试不仅涉及自动化测试,还涉及全面的手动测试。然而,如果能够增加自动化测试覆盖范围,许多方面都可以得到改进,从而使团队能够更早地发现错误并缩短生产周期。
在本系列中,我将从最常见和最简单的测试实践开始,介绍其功能和底层概念。
单元测试
什么是单元测试?
单元测试是一种软件测试技术,用于测试软件应用程序中的各个单元或组件,使其与应用程序的其余部分隔离开来。单元可以是函数、方法、类,甚至是模块。单元测试的目的是验证软件的每个单元是否按预期运行并满足其预期用途。
为什么单元测试很重要?
单元测试之所以重要,原因如下:
- 早期发现缺陷:单元测试有助于在开发周期早期发现缺陷和问题,从而减少总体时间和成本。
- 增强对软件的信心:单元测试在软件与其他组件集成之前提供了对软件功能和性能的信心。
- 减少回归问题:单元测试有助于确保代码更改和更新不会引入新的缺陷或问题。
- 支持重构和维护:单元测试通过确保更改不会破坏现有功能来支持代码库的重构和维护。
单元测试的优点和缺点:
单元测试的一些优点包括:
- 提高代码质量和可靠性
- 更快地检测和解决缺陷
- 更好的可维护性和可扩展性
- 提高开发人员的信心和生产力
单元测试的一些缺点包括:
- 维护成本
- 随着代码库的增长,测试维护可能会成为一项巨大的开销,需要更新来反映代码的变化。
- 系统架构的变化可能会使单元测试过时,需要进行大量返工来更新测试。
- 测试可能会变得脆弱并需要不断维护,特别是当它们与正在测试的代码紧密耦合时。
- 测试复杂系统的困难
- 单元测试可能无法涵盖复杂系统中可能出现的所有可能情况。
- 过度依赖单元测试,而忽略了集成和系统级测试
- 错误的安全感,认为代码没有错误。
- 范围有限,因为单元测试仅测试孤立代码,而不是不同组件之间的交互。
- 如果仅在集成或系统级别发现问题,则返工成本高昂。
- 集成和系统级测试可以发现单元测试可能遗漏的问题,例如性能问题、安全问题或兼容性问题。
正确测试的有用技术:
- 依赖注入:此技术涉及通过构造函数、方法参数或属性将依赖项传递给被测代码。这使得在测试期间可以轻松地用模拟对象替换依赖项。
- 测试数据构建器:此技术涉及创建具有默认值的测试数据对象,这些默认值可根据特定测试的需要进行覆盖。这使得创建具有不同数据场景的测试用例变得容易。
- 代码覆盖率分析:这项技术涉及测量测试期间执行的代码量。它有助于识别代码中未经测试的区域,并有助于提高代码的整体质量。
- 基于属性的测试:此技术涉及根据代码预期满足的一组属性生成大量测试用例。这有助于捕获少量手动创建的测试用例可能无法覆盖的边缘情况和极端情况。
- 变异测试:这项技术涉及对被测代码进行细微修改以创建新版本,并针对每个版本运行测试套件。通过测量有多少变异版本仍然通过测试,这有助于识别测试套件中的缺陷。
- 模拟对象:此技术涉及创建模拟真实对象行为的虚假对象,以测试被测代码与其依赖项之间的交互。模拟对象可用于模拟外部服务、数据库或复杂依赖项的行为。
- OmniMock:这项技术涉及使用一个工具,该工具可以自动为给定代码模块的所有依赖项生成模拟对象。这有助于减少编写测试用例所需的样板代码量,并使测试具有许多依赖项的复杂代码变得更加容易。
如何自动化单元测试:
单元测试自动化在开发过程的每个阶段都至关重要,但随着代码库规模和复杂性的增长,它变得越来越重要。自动化单元测试可以帮助在开发过程的早期发现错误,从而减少调试和修复问题所需的成本和时间。此外,自动化使开发人员能够快速高效地运行测试,从而更快地获得代码更改的反馈。
以下是单元测试自动化的一些最佳实践:
- 保持测试轻量级。单元测试应该快速且易于运行。测试应专注于一小部分功能,避免同时测试多个组件或系统。通过保持测试轻量级,开发人员可以频繁运行测试,尽早发现问题,并确保测试持续有效。
- 使用模拟和存根将测试单元与其依赖项隔离。这可确保测试保持轻量级且运行速度快。
- 将测试集成到开发流程中,以便在每次代码更改时自动运行。这可确保开发人员能够立即收到有关其代码更改的反馈,并在开发过程中尽早发现任何问题。
- 使用持续集成工具(例如 Jenkins 或 Travis CI)来自动化测试流程。这些工具可以自动运行测试并立即提供问题反馈。通过自动化测试流程,开发人员可以专注于编写代码,而无需手动运行测试。
- 使用 Git 等版本控制软件来跟踪代码库的更改。这样可以轻松查看更改者、更改时间以及更改了哪些内容。开发人员应确保在合并任何更改之前运行测试,以防止任何损坏的代码合并到代码库中。
例子
在下面的例子中,我们可以注意到,即使是像“divide”这样简单的函数,也可以通过多种方式进行测试,以确保其正常工作,并且其预期行为不会受到代码更改的影响。测试列表可能并不全面,但它很好地证明了正确测试的重要性。
import unittest
def divide(a, b):
return a / b
class TestDivide(unittest.TestCase):
def test_divide_by_positive_integers(self):
self.assertEqual(divide(10, 2), 5)
self.assertEqual(divide(4, 2), 2)
def test_divide_by_zero(self):
self.assertRaises(ZeroDivisionError, divide, 10, 0)
def test_divide_by_float(self):
self.assertAlmostEqual(divide(1, 3), 0.33333333333, places=10)
def test_zero divident(self):
self.assertEqual(divide(0, 1), 0)
def test_self_division(self):
self.assertEqual(divide(1, 1), 1)
def test_negative_division(self):
self.assertEqual(divide(-10, -2), 5)
self.assertEqual(divide(-10, 2), -5)
self.assertEqual(divide(10, -2), -5)
def test_divide_by_nonnumeric_input(self):
self.assertRaises(TypeError, divide, "10", 2)
self.assertRaises(TypeError, divide, 10, "2")
def test_divide_overflow(self):
self.assertRaises(OverflowError, divide, float("inf"), 1)
def test_large_integer_division(self):
self.assertEqual(divide(999999999999999, 3), 333333333333333)
self.assertEqual(divide(-999999999999999, 3), -333333333333333)
测试用例描述
self.assertEqual(divide(10, 2), 5)
和self.assertEqual(divide(4, 2), 2)
- 这些测试检查当我们将 10 除以 2 或将 4 除以 2 时,除法是否返回正确的值,分别应该是 5 或 2。self.assertRaises(ZeroDivisionError, divide, 10, 0)
ZeroDivisionError
- 此测试检查当我们尝试将 10 除以 0 时,除法是否会产生 a ,这是一个无效操作。self.assertAlmostEqual(divide(1, 3), 0.33333333333, places=10)
- 此测试检查除法返回的值是否接近预期值,容差为小数点后 10 位。当预期值为小数或分数时,此测试非常有用。self.assertEqual(divide(0, 1), 0)
- 此测试检查当我们用非零数除 0 时,除法是否返回 0。self.assertEqual(divide(1, 1), 1)
- 此测试检查当我们用一个数字除以自身时,除法是否返回 1。self.assertEqual(divide(-10, -2), 5)
- 此测试检查除法是否正确适用于负数。self.assertEqual(divide(-10, 2), -5)
和self.assertEqual(divide(10, -2), -5)
- 这些测试检查除法在混合符号的情况下是否正常工作。self.assertRaises(TypeError, divide, "10", 2)
和self.assertRaises(TypeError, divide, 10, "2")
- 这些测试检查当我们传入非数字参数时,divide 方法是否会引发 TypeError。self.assertRaises(OverflowError, divide, float("inf"), 1)
OverflowError
此测试用例检查当我们尝试将一个非常大的数字除以 1 时,除法是否会产生结果。self.assertEqual(divide(999999999999999, 3), 333333333333333)
- 此测试用例检查除法函数是否正确处理较大的正整数。self.assertEqual(divide(-999999999999999, 3), -333333333333333)
- 此测试用例检查除法函数是否正确处理较大的负整数。
通过包含这些额外的测试用例,我们可以更加确信除法能够正常工作并处理不同类型的输入值。
结论
总而言之,单元测试是一项重要的软件测试技术,有助于确保软件应用程序的质量和可靠性。单元测试可以使用多种编程语言执行,并且单元测试框架和库可以更轻松地编写和运行单元测试。自动化测试工具和框架可以帮助简化编写和运行单元测试的过程,从而节省时间并提高整体效率。
冒烟测试
什么是烟雾测试?
冒烟测试,也称为“构建验证测试”,是一种验证应用程序基本功能的软件测试。冒烟测试的目的是确保软件的关键功能正常运行,并在进行更深入的测试之前检测出任何重大缺陷。
“冒烟测试”一词源自硬件测试,即电子设备首次启动时,如果未着火,则进行“冒烟测试”。同样,在软件测试中,冒烟测试是指在进一步测试之前进行的快速测试,以查看系统是否着火(崩溃)。
冒烟测试通常在新软件构建完成后执行,旨在验证软件能否正确执行其基本功能。这些测试通常手动或自动执行,涵盖软件的最关键功能。
烟雾测试示例:
以下是一些烟雾测试场景的示例:
- 验证应用程序是否可以启动而不会崩溃
- 检查登录功能是否正常工作
- 确认数据库连接正常
- 确保数据可以保存并从数据库中检索
- 验证关键 UI 元素是否可见且功能正常
为什么烟雾测试很重要:
冒烟测试至关重要,因为它能够在开发过程的早期发现关键缺陷。通过及早发现缺陷,开发人员可以在问题变得更加严重、需要投入更多时间和资源来解决之前进行修复。
冒烟测试还可以识别其他测试方法可能无法发现的问题,从而帮助提高软件质量。这最终可以节省时间和金钱,并改善应用程序的用户体验。
冒烟测试专注于验证系统的基本功能,通常从高层次进行。它们通常用于快速识别可能影响应用程序正常运行的重大问题,并在更深入的测试之前运行,以便尽早发现任何重大问题。虽然冒烟测试对于检测应用程序整体功能中的主要问题很有用,但在验证单个代码单元的正确性方面,它们无法提供与单元测试同等程度的详细程度。相反,单元测试可能无法捕获多个代码单元组合时出现的问题,而这正是冒烟测试的用武之地。
烟雾测试的优缺点:
烟雾测试的一些优点包括:
- 在开发过程的早期发现关键缺陷
- 帮助提高软件质量
- 通过及早发现问题来节省时间和金钱
- 可以提高应用程序的用户体验
烟雾测试的一些缺点包括:
- 仅涵盖基本功能
- 如果没有进行更彻底的测试,可能会给人一种虚假的安全感
- 需要时间和资源来设置和执行
- 可能会错过烟雾测试期间不明显的一些缺陷
如何自动化烟雾测试:
自动化烟雾测试有助于减少执行烟雾测试所需的时间和精力。以下是自动化烟雾测试的一些步骤:
- 确定要测试的软件的关键特性
- 选择自动化工具或框架
- 编写测试脚本来自动化测试
- 将测试集成到开发过程中
- 安排测试在每次构建后自动运行
- 使用持续集成工具,例如 Jenkins 或 Travis CI
- 使用 Git 等版本控制软件来跟踪代码库的变化。
例子
假设我们有一个使用 Python 和 Flask 框架构建的 RESTful API 服务。该服务有多个端点,包括一个从数据库检索产品列表的 GET 端点。
我们可以创建一个烟雾测试来验证以下内容:
- 服务可以正常启动。
- GET 端点返回状态代码为 200 的响应。
- GET 端点的响应包含产品列表。
下面是如何使用 PyTest 库对此 Python 服务实施冒烟测试的示例:
- 使用 pip 安装 PyTest:
$ pip install pytest
- 在项目的根目录中创建一个名为 test_smoke.py 的测试文件。
- 导入必要的模块:
import requests
import json
- 定义启动服务的装置:
import pytest
from myapp import create_app
@pytest.fixture(scope="session")
def app():
app = create_app()
app_context = app.app_context()
app_context.push()
yield app
app_context.pop()
在这里,我们创建一个名为 app 的装置,它通过调用 myapp 模块中定义的 create_app() 函数来启动 Flask 应用程序。
- 定义一个测试函数,向服务发送 GET 请求并验证响应:
def test_get_products(app):
response = requests.get('http://localhost:5000/products')
assert response.status_code == 200
data = json.loads(response.text)
assert isinstance(data, list)
assert len(data) > 0
在这里,我们向 /products 端点发送 GET 请求并验证响应的状态代码是否为 200。我们还检查响应是否包含产品列表。
使用 PyTest 运行测试:
$ pytest test_smoke.py
这将执行测试并输出结果。如果测试通过,你应该看到如下消息:
============================= test session starts ==============================
collected 1 item
test_smoke.py . [100%]
============================== 1 passed in 0.12s ==============================
如果测试失败,PyTest 将提供有关失败的详细信息。
通过在每次构建后运行此测试,我们可以确保服务正常运行,然后再进行更深入的测试。
结论
冒烟测试是一种专注于验证系统基本功能的测试,通常从高层次进行。它通常用于快速识别可能影响应用程序正常运行的重大问题,并在更深入的测试之前运行,以便尽早发现任何重大问题。虽然冒烟测试可能不如其他类型的测试那么详细,但它们在开发过程中起着至关重要的作用。
需要注意的是,自动化冒烟测试不应该是唯一的测试方法。进行更深入的测试以识别所有可能的缺陷仍然至关重要。
验收测试
什么是验收测试?
验收测试是一种软件测试,用于验证软件应用程序是否满足指定要求并准备好部署到生产环境。它通常在单元测试和集成测试之后、软件发布给最终用户之前进行。
与冒烟测试不同,验收测试旨在测试软件是否满足利益相关者或最终用户的需求和规范。验收测试通常在开发阶段完成后、软件发布给最终用户之前进行。验收测试的目标是确保软件适合发布并满足利益相关者的期望。验收测试通常手动完成,可能涉及根据用户故事、用户工作流或其他规范创建测试用例。
验收测试的示例包括:
- 用户验收测试 (UAT) - 最终用户测试软件以验证其是否满足他们的需求和要求。
- 业务验收测试 (BAT) - 组织业务方面的利益相关者测试软件以确保其符合业务目标和流程。
- 操作验收测试 (OAT) - 在类似生产的环境中测试软件,以确保其能够顺利部署和运行。
为什么验收测试很重要:
- 确保软件满足要求:验收测试对于确保软件满足利益相关者和最终用户指定的要求至关重要。
- 防止缺陷进入生产:验收测试有助于在开发过程的早期发现缺陷,从而可以防止代价高昂的缺陷进入生产,从而节省时间和金钱。
- 提高软件质量:验收测试有助于识别缺陷并确保软件按预期运行,从而提高软件质量。
- 增强利益相关者的信心:当验收测试成功通过时,利益相关者(包括最终用户、企业主和项目经理)会对软件的功能充满信心。
验收测试的优缺点:
验收测试的一些优点包括:
- 提高软件质量。
- 防止缺陷进入生产。
- 提高利益相关者的信心。
- 明确指示软件何时可以发布。
验收测试的一些缺点包括:
- 可能很耗时且昂贵。
- 需要与利益相关者进行大量规划和协调。
- 测试可能不够详尽,也可能无法发现所有缺陷。
- 结果可能是主观的,取决于利益相关者的解释。
如何自动化验收测试:
- 定义涵盖软件关键功能和要求的测试场景。
- 编写测试脚本,自动化测试场景并验证软件的功能。
- 运行自动化测试来验证软件的功能并识别缺陷。
- 根据需要重复。不断更新和完善测试场景和测试脚本,以确保软件保持正常运行并满足利益相关者的要求。
- 将测试集成到开发过程中
- 安排测试在每次构建后自动运行
- 使用持续集成工具,例如 Jenkins 或 Travis CI
- 使用 Git 等版本控制软件来跟踪代码库的变化。
例子
假设我们有一个交易平台,允许用户买卖货币对。我们想测试在给定汇率下,用欧元下单买入美元的功能。
def place_market_order(
usd_balance: float, eur_balance: float, exchange_rate: float, order_quantity: float
) -> tuple[float, float]:
cost = order_quantity / exchange_rate
updated_usd_balance = usd_balance - cost
updated_eur_balance = eur_balance + order_quantity
return updated_usd_balance, updated_eur_balance
以下是我们使用 PyTest 为该场景编写验收测试的方法:
import pytest
def test_market_order_usd_eur():
# Arrange
usd_balance = 1000.00
eur_balance = 500.00
exchange_rate = 1.20
# Act
updated_usd_balance, updated_eur_balance = place_market_order(
usd_balance, eur_balance, exchange_rate, order_quantity=250.00
)
# Assert
assert updated_usd_balance == 700.00
assert updated_eur_balance == 750.00
测试描述
在这个例子中,我们首先定义一个名为 的测试函数
test_market_order_usd_eur()
。在 Arrange 部分,我们为用户设置了 USD 和 EUR 的初始余额,以及 USD 和 EUR 之间的当前汇率。
在“操作”部分,我们模拟下达一笔以欧元买入美元的市价单,数量为 250.00。我们使用当前汇率计算该订单的欧元成本,从用户的美元余额中扣除成本,并将数量添加到用户的欧元余额中。
最后,在 Assert 部分,我们验证用户的余额是否已根据汇率和订单数量正确更新。具体来说,我们检查美元余额是否已减少正确的金额,欧元余额是否已增加正确的金额。
然后,我们可以使用 PyTest 运行此测试,它将自动执行代码以下达市价单,并验证用户的余额是否已正确更新。如果测试通过,我们可以确信交易平台运行正常,并符合我们下达使用欧元买入美元的市价单的验收标准。
验收测试和单元测试之间的区别。
细心的读者可能会注意到,上面提供的示例看起来与单元测试的实践类似。然而,验收测试与单元测试在几个方面有所不同。
- 范围:验收测试侧重于测试整个系统,而单元测试侧重于单独测试系统的各个单元或组件。
- 目的:验收测试的目的是确保系统满足利益相关者的要求和期望,而单元测试的目的是捕获错误并确保系统的各个组件正常工作。
- 协作:验收测试通常涉及与利益相关者和最终用户的协作,以编写和执行模拟真实使用场景的测试,而单元测试通常由与利益相关者和最终用户隔离的开发人员执行。
- 自动化程度:验收测试可以手动或自动化,但通常采用自动化测试以确保可重复性和一致性。另一方面,单元测试通常采用自动化测试以确保效率并快速捕获回归问题。
总而言之,验收测试和单元测试服务于不同的目的,并且在系统的不同级别执行。验收测试侧重于确保系统满足利益相关者的要求和期望,而单元测试则侧重于捕获错误并确保系统的各个组件正常运行。
结论
总而言之,验收测试是软件开发的关键组成部分,它确保软件满足利益相关者的要求并按预期运行。通过自动化验收测试,团队可以节省时间并降低成本,同时提高软件质量和利益相关者的信心。
流行的测试库:
测试框架和库使编写和运行单元测试变得更加容易。以下是一些适用于多种编程语言的常见单元测试库示例:
- C/C++:CppUTest、CppUnit、Google Test。
- Python:PyTest、Behave、Robot Framework。
- Java:JUnit、TestNG、Cucumber。
- Golang:GoConvey、GoTest、Ginkgo。