在我的第一篇文章《测试用例设计心态一览》中,我讨论了如何在创建测试用例时确保质量,使用了下订单 API 和排球类比。虽然那篇文章的重点是发现漏洞,但在当今快节奏的软件开发环境中,预防漏洞变得更加至关重要。选择合适的测试策略是确保产品质量、加快发布速度并避免生产环境中昂贵漏洞的关键。
一种广为人知的平衡测试方法是测试金字塔(Test Pyramid),由 Mike Cohn 在他 2009 年的书《成功使用敏捷:使用 Scrum 的软件开发》中推广。这个概念源于 2000 年代初的讨论,Martin Fowler 和 Jason Huggins 等人独立探索了类似的想法并做出了贡献。
在本文中,我将基于之前的讨论,再次使用下订单 API 和排球类比,解释软件开发中测试金字塔的关键概念,并通过实际例子说明每一层。
什么是测试金字塔?
测试金字塔是一种测试策略,由不同层次的测试组成,呈金字塔状排列,并集成到软件部署管道中。随着我们向金字塔的每一层移动,测试在范围、速度和成本上有所不同:
- 范围:测试覆盖的功能广度。
- 速度:测试执行的速度以及发现和修复漏洞的速度。
- 成本:维护测试和解决漏洞所需的资源。
随着我们向金字塔的上层移动,测试数量减少,因为高层次的测试执行时间更长,覆盖范围更广,增加了复杂性和成本。这就是为什么金字塔的基础(单元测试)包含大部分测试,因为它们快速且成本低,而顶部(端到端测试)则有较少、更复杂的测试。
单元测试
单元测试是测试金字塔的基础,旨在隔离地测试代码的单个部分。它们专注于确保小的、具体的函数或方法按预期工作。
以下是下订单服务中 calculate_order_total
函数的单元测试示例:
# place_order_api.py(下订单的 API 端点)
def calculate_order_total(cart_items):
"""计算购物车中商品总价的函数"""
total = 0
for item in cart_items:
total += item['price'] * item['quantity']
return total
# test_order.py(单元测试)
import unittest
from order import calculate_order_total
class TestPlaceOrder(unittest.TestCase):
def test_calculate_order_total(self):
cart_items = [
{'name': 'Volleyball', 'price': 1000, 'quantity': 10},
{'name': 'Ball Cart', 'price': 50, 'quantity': 1}
]
total = calculate_order_total(cart_items)
self.assertEqual(total, 10050)
def test_negative_price(self):
cart_items = [
{'name': 'Volleyball', 'price': -1000, 'quantity': 10}
]
total = calculate_order_total(cart_items)
self.assertEqual(total, -10000)
if __name__ == '__main__':
unittest.main()
现在,让我们用排球术语来思考。单元测试就像球员练习个人技能,例如发球、传球、扣球或拦网。这些是比赛的基本构建模块。每个球员专注于掌握自己的技术,然后才能作为一个团队协作。
同样,单元测试专注于代码中的一个小、隔离的部分,以确保其正确执行,就像练习发球确保球员始终准确无误一样。
集成测试
集成测试位于单元测试之上,验证不同组件或服务之间的交互。它们对于识别在隔离单元内可能不会出现的问题至关重要,但在两个服务(提供者和消费者)之间的通信格式不匹配时可能会出现问题。
例如,/place_order
API 的集成测试确保订单处理逻辑与外部支付服务正确交互,防止因期望不匹配而产生的错误:
# place_order_api.py(下订单的 API 端点)
def process_payment(payment_details, amount):
"""调用外部支付服务的函数"""
payment_service_url = "http://external-payment-service.com/api/process-payment"
payment_payload = {
"card_number": payment_details['card_number'],
"amount": amount
}
response = requests.post(payment_service_url, json=payment_payload)
if response.status_code == 200:
return True
return False
@app.route('/place-order', methods=['POST'])
def place_order():
data = request.get_json()
cart_items = data.get('cart_items')
payment_details = data.get('payment_details')
# 计算订单总金额
total_amount = calculate_order_total(cart_items)
# 检查支付是否可以处理
if not process_payment(payment_details, total_amount):
return jsonify({"message": "Payment failed"}), 400
# 订单成功下达
return jsonify({"message": "Order placed successfully", "order_id": 123, "total_amount": total_amount}), 200
if __name__ == '__main__':
app.run()
# test_order_api.py(下订单 API 的集成测试)
import requests
import unittest
class TestPlaceOrderAPI(unittest.TestCase):
def test_place_order_success(self):
response = requests.post('http://localhost:5000/place-order', json={
'cart_items': [{'name': 'Volleyball', 'price': 1000, 'quantity': 10}],
'payment_details': {'card_number': 'valid'}
})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json().get('message'), 'Order placed successfully')
def test_place_order_payment_failed(self):
response = requests.post('http://localhost:5000/place-order', json={
'cart_items': [{'name': 'Ball Cart', 'price': 50, 'quantity': 1}],
'payment_details': {'card_number': 'invalid'}
})
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json().get('message'), 'Payment failed')
if __name__ == '__main__':
unittest.main()
想象一个排球队,每个球员都有特定的角色——二传负责将球传给扣球手,自由人则负责防守。一次进攻的成功取决于这些角色的整合程度;如果二传的传球有误,扣球手就无法执行成功的攻击。这涉及到测试团队合作和协调,以确保团队能够有效地协作。就像软件应用依赖于不同组件无缝协作一样。
端到端测试
测试金字塔的顶部是端到端测试(End-to-End Tests),它们通过模拟实际用户的完整旅程,为团队提供最大的信心。
例如,在下订单流程中,端到端测试将模拟一个用户从将商品添加到购物车、进行结账、输入支付详情到确认订单的整个旅程。
// place_order.spec.js(使用 Playwright 的端到端测试)
const { test, expect } = require('@playwright/test');
test.describe('Place Order Flow', () => {
test('should successfully place an order', async ({ page }) => {
// 访问电子商务网站
await page.goto('http://localhost:3000');
// 将商品添加到购物车
await page.click('text=Volleyball');
await page.click('text=Add to Cart');
// 访问购物车页面
await page.click('text=Cart');
// 进行结账
await page.click('text=Checkout');
// 填写收货和支付详情
await page.fill('#shipping-address', '123 Main St, City, Country');
await page.fill('#card-number', 'valid');
// 下订单
await page.click('text=Place Order');
// 断言成功信息
await expect(page.locator('text=Order placed successfully')).toBeVisible();
});
});
最后,在排球中,技能的终极考验是进行一场完整的比赛。在这里,一切汇聚在一起——发球、传球、扣球、拦网——在真实的比赛条件下进行。这种情景揭示了球员在压力下的表现,展示了他们策略和团队合作在真实比赛中的有效性。
理解测试金字塔后,让我们将其与冰淇淋锥反模式进行对比:
测试金字塔与冰淇淋锥对比
什么是冰淇淋锥?
一个常见的反模式是冰淇淋锥,它与金字塔相反。其狭窄的基础使其不稳定,导致测试执行缓慢且维护成本高。大部分测试集中在顶部,管理起来变得困难——就像试图从卖家那里拿起一支土耳其冰淇淋锥一样。
这种反模式的一个例子是过度依赖完整的端到端订单流程,将支付、运输和通知捆绑到一个缓慢运行的测试中。这种方法导致只有有限的测试来验证订单和支付服务之间的重要交互,造成系统通信中的潜在漏洞。此外,基本功能(如计算订单总额)的单元测试很少,导致关键功能未被测试,增加了未检测到漏洞的风险。
想象一个排球队只进行完整的比赛,专注于整个比赛,而很少关注个人技能。对传球训练或二传技术的关注最少,导致比赛的关键领域被忽视。结果,团队在基本技能上可能会有困难,导致比赛中的表现不佳。
同样,在软件开发中,仅依赖端到端测试而没有足够的单元测试,会导致核心功能未被测试,可能引发本可以在早期轻松发现的问题。
总结
采用结构化的测试策略,例如测试金字塔,对于确保软件质量和维护高效的工作流程至关重要。虽然单元测试和端到端测试都提供了有价值的覆盖并增强了用户体验,但关键是要仔细构建我们的测试方法,以实现平衡和效率。
为了进一步说明这些概念,我们可以用排球类比:
- 单元测试 = 练习个人技能:快速、简单,专注于小任务,如发球或传球。在测试中,它是检查代码的小部分。
- 集成测试 = 团队训练:检查球员如何协作。在测试中,它是确保系统的不同部分(如支付和订单之间的传递)良好沟通。
- 端到端测试 = 完整比赛:进行整个比赛。在测试中,它是像用户一样运行软件,从头到尾,确保一切顺利运行。
不同的项目、技术和团队结构需要不同的方法。没有一种方法适用于所有情况。工程师们应该共同努力,识别最有效的测试策略,将测试分布在不同的层次,允许在开发周期的早期发现问题。通过理解这些概念,我们可以更好地导航我们的测试策略,提升整体软件质量。(以及排球比赛)
感谢阅读,让我们继续分享,使你的工作像你的比赛一样有趣。