【测试】自动化测试实战:从单元测试到端到端测试
前言
软件测试是保证软件质量的重要手段,自动化测试更是现代软件开发流程中不可或缺的环节。随着敏捷开发和DevOps的普及,自动化测试已经从"可选项"变成了"必选项"。一个没有完善自动化测试覆盖的项目,就像一栋没有地基的高楼,随时可能因为一个看似微小的改动而轰然倒塌。
作为AI程序员,我深知测试的重要性------无论是训练模型还是部署服务,可靠的测试都能帮助我们及时发现问题、保证质量、降低风险。本文将系统性地介绍自动化测试的各个层面,从单元测试到集成测试,从端到端测试到性能测试,通过大量的Python实战代码,帮助读者建立完整的测试知识体系。
一、测试基础理论
1.1 测试金字塔
测试金字塔是一种指导测试策略的经典模型,它将测试分为不同的层次,自底向上分别是:单元测试、集成测试、端到端测试(E2E测试)。
/\
/ \
/ E2E \ <- 少量、耗时、覆盖关键路径
/--------\
/ 集成测试 \ <- 中等数量、覆盖模块交互
/------------\
/ 单元测试 \ <- 大量、快速、覆盖核心逻辑
/----------------\
- 单元测试:测试最小的可测试单元,通常是函数或方法,要求快速、隔离、可重复
- 集成测试:测试多个单元之间的交互,确保模块之间正确协作
- 端到端测试:从用户视角测试完整的功能流程,模拟真实使用场景
测试金字塔告诉我们:应该大量编写单元测试作为基础,适量编写集成测试覆盖模块交互,少量编写端到端测试验证关键路径。
1.2 测试驱动开发(TDD)
测试驱动开发(TDD)是一种软件开发方法论,其核心思想是"先写测试,再写代码"。TDD的循环是:红(Red)-> 绿(Green)-> 重构(Refactor)。
python
# TDD示例:实现一个简单的计算器
# 第一步:写测试(Red)
# 此时功能还未实现,测试应该失败
import unittest
class TestCalculator(unittest.TestCase):
def test_add_two_numbers(self):
calc = Calculator()
result = calc.add(2, 3)
self.assertEqual(result, 5)
def test_add_negative_numbers(self):
calc = Calculator()
result = calc.add(-1, -1)
self.assertEqual(result, -2)
def test_add_zero(self):
calc = Calculator()
result = calc.add(5, 0)
self.assertEqual(result, 5)
# 第二步:写代码使测试通过(Green)
class Calculator:
def add(self, a, b):
return a + b
# 第三步:重构 - 优化代码但保持测试通过
1.3 测试的FIRST原则
优秀的单元测试应该满足FIRST原则:
- Fast(快速):测试应该能够快速执行,以便频繁运行
- Independent(独立):每个测试应该独立运行,不依赖其他测试
- Repeatable(可重复):测试应该每次都产生相同的结果,不受外部环境影响
- Self-validating(自验证):测试应该能够自动判断通过还是失败
- Timely(及时):测试应该在实际代码之前编写(TDD)
二、单元测试实战
2.1 unittest框架
Python标准库提供了unittest框架,用于编写和运行单元测试。
python
import unittest
from typing import List, Optional
class User:
"""用户类"""
def __init__(self, user_id: int, username: str, email: str):
self.user_id = user_id
self.username = username
self.email = email
self.is_active = True
def deactivate(self):
self.is_active = False
def activate(self):
self.is_active = True
def __repr__(self):
return f"User({self.user_id}, {self.username})"
class UserRepository:
"""用户仓储(模拟)"""
def __init__(self):
self._users: List[User] = []
self._next_id = 1
def create(self, username: str, email: str) -> User:
if any(u.email == email for u in self._users):
raise ValueError(f"Email {email} already exists")
user = User(self._next_id, username, email)
self._users.append(user)
self._next_id += 1
return user
def find_by_id(self, user_id: int) -> Optional[User]:
for user in self._users:
if user.user_id == user_id:
return user
return None
def find_by_email(self, email: str) -> Optional[User]:
for user in self._users:
if user.email == email:
return user
return None
def all(self) -> List[User]:
return self._users.copy()
def delete(self, user_id: int) -> bool:
for i, user in enumerate(self._users):
if user.user_id == user_id:
del self._users[i]
return True
return False
class TestUserRepository(unittest.TestCase):
"""用户仓储测试"""
def setUp(self):
"""每个测试方法前执行,用于初始化测试环境"""
self.repo = UserRepository()
def tearDown(self):
"""每个测试方法后执行,用于清理测试环境"""
pass
def test_create_user(self):
"""测试创建用户"""
user = self.repo.create("alice", "alice@example.com")
self.assertIsNotNone(user)
self.assertEqual(user.username, "alice")
self.assertEqual(user.email, "alice@example.com")
self.assertTrue(user.is_active)
self.assertEqual(user.user_id, 1)
def test_create_duplicate_email_raises_error(self):
"""测试重复邮箱应该抛出异常"""
self.repo.create("alice", "alice@example.com")
with self.assertRaises(ValueError) as context:
self.repo.create("bob", "alice@example.com")
self.assertIn("already exists", str(context.exception))
def test_find_by_id(self):
"""测试按ID查找用户"""
created_user = self.repo.create("alice", "alice@example.com")
found_user = self.repo.find_by_id(created_user.user_id)
self.assertIsNotNone(found_user)
self.assertEqual(found_user.user_id, created_user.user_id)
self.assertEqual(found_user.username, "alice")
def test_find_by_nonexistent_id(self):
"""测试查找不存在的ID"""
result = self.repo.find_by_id(999)
self.assertIsNone(result)
def test_find_by_email(self):
"""测试按邮箱查找用户"""
self.repo.create("alice", "alice@example.com")
found_user = self.repo.find_by_email("alice@example.com")
self.assertIsNotNone(found_user)
self.assertEqual(found_user.username, "alice")
def test_delete_user(self):
"""测试删除用户"""
user = self.repo.create("alice", "alice@example.com")
result = self.repo.delete(user.user_id)
self.assertTrue(result)
self.assertIsNone(self.repo.find_by_id(user.user_id))
def test_delete_nonexistent_user(self):
"""测试删除不存在的用户"""
result = self.repo.delete(999)
self.assertFalse(result)
def test_all_returns_copy(self):
"""测试all方法返回列表副本"""
user = self.repo.create("alice", "alice@example.com")
users = self.repo.all()
users.clear() # 修改返回的列表
self.assertEqual(len(self.repo.all()), 1) # 原始列表不受影响
if __name__ == "__main__":
unittest.main(verbosity=2)
2.2 pytest框架
pytest是Python最流行的第三方测试框架,它比unittest更加简洁、强大。
bash
# 安装pytest
pip install pytest pytest-cov pytest-mock
python
import pytest
from typing import List, Optional
class Order:
"""订单类"""
def __init__(self, order_id: int, user_id: int, items: List[dict]):
self.order_id = order_id
self.user_id = user_id
self.items = items
self.status = "pending"
@property
def total_amount(self) -> float:
return sum(item["price"] * item["quantity"] for item in self.items)
def apply_discount(self, discount: float) -> float:
if discount < 0 or discount > 1:
raise ValueError("Discount must be between 0 and 1")
return self.total_amount * (1 - discount)
def can_cancel(self) -> bool:
return self.status in ("pending", "confirmed")
def cancel(self):
if not self.can_cancel():
raise ValueError(f"Cannot cancel order with status: {self.status}")
self.status = "cancelled"
# pytest测试类
class TestOrder:
"""订单测试"""
def test_total_amount_calculation(self):
"""测试订单总额计算"""
items = [
{"name": "Apple", "price": 5.0, "quantity": 3},
{"name": "Banana", "price": 2.5, "quantity": 2},
]
order = Order(1, 100, items)
assert order.total_amount == 20.0 # 5*3 + 2.5*2
def test_total_amount_empty_order(self):
"""测试空订单总额"""
order = Order(1, 100, [])
assert order.total_amount == 0
def test_apply_discount(self):
"""测试折扣应用"""
items = [{"name": "Apple", "price": 10.0, "quantity": 1}]
order = Order(1, 100, items)
final_price = order.apply_discount(0.1) # 10%折扣
assert final_price == 9.0
def test_invalid_discount_raises_error(self):
"""测试无效折扣抛出异常"""
items = [{"name": "Apple", "price": 10.0, "quantity": 1}]
order = Order(1, 100, items)
with pytest.raises(ValueError) as exc:
order.apply_discount(1.5) # 无效折扣
assert "between 0 and 1" in str(exc.value)
def test_cancel_pending_order(self):
"""测试取消待处理订单"""
order = Order(1, 100, [])
order.cancel()
assert order.status == "cancelled"
def test_cancel_confirmed_order(self):
"""测试取消已确认订单"""
order = Order(1, 100, [])
order.status = "confirmed"
order.cancel()
assert order.status == "cancelled"
def test_cancel_shipped_order_raises_error(self):
"""测试取消已发货订单失败"""
order = Order(1, 100, [])
order.status = "shipped"
with pytest.raises(ValueError) as exc:
order.cancel()
assert "Cannot cancel" in str(exc.value)
def test_can_cancel_for_different_statuses(self):
"""测试不同状态下是否可以取消"""
order = Order(1, 100, [])
order.status = "pending"
assert order.can_cancel() is True
order.status = "confirmed"
assert order.can_cancel() is True
order.status = "shipped"
assert order.can_cancel() is False
order.status = "delivered"
assert order.can_cancel() is False
# 使用pytest fixtures
@pytest.fixture
def sample_order():
"""订单fixture"""
items = [
{"name": "Laptop", "price": 5000.0, "quantity": 1},
{"name": "Mouse", "price": 100.0, "quantity": 2},
]
return Order(1, 100, items)
class TestOrderWithFixtures:
"""使用fixture的订单测试"""
def test_order_total_with_fixture(self, sample_order):
"""使用fixture测试订单总额"""
assert sample_order.total_amount == 5200.0
def test_order_discount_with_fixture(self, sample_order):
"""使用fixture测试折扣"""
final_price = sample_order.apply_discount(0.2)
assert final_price == 4160.0
2.3 参数化测试
python
import pytest
from typing import List
def find_max(numbers: List[int]) -> int:
"""查找列表中的最大值"""
if not numbers:
raise ValueError("List cannot be empty")
return max(numbers)
def find_min(numbers: List[int]) -> int:
"""查找列表中的最小值"""
if not numbers:
raise ValueError("List cannot be empty")
return min(numbers)
class TestFindMax:
"""find_max函数测试"""
# 参数化测试
@pytest.mark.parametrize("numbers,expected", [
([1, 2, 3, 4, 5], 5),
([5, 4, 3, 2, 1], 5),
([3, 3, 3, 3], 3),
([-5, -2, -10, -1], -1),
([100], 100),
])
def test_find_max_various_inputs(self, numbers, expected):
"""参数化测试:各种输入"""
assert find_max(numbers) == expected
def test_find_max_empty_raises_error(self):
"""测试空列表抛出异常"""
with pytest.raises(ValueError) as exc:
find_max([])
assert "cannot be empty" in str(exc.value)
class TestFindMin:
"""find_min函数测试"""
@pytest.mark.parametrize("numbers,expected", [
([1, 2, 3, 4, 5], 1),
([5, 4, 3, 2, 1], 1),
([3, 3, 3, 3], 3),
([-5, -2, -10, -1], -10),
])
def test_find_min_various_inputs(self, numbers, expected):
assert find_min(numbers) == expected
三、集成测试实战
3.1 模块间交互测试
python
import pytest
from unittest.mock import Mock, patch, MagicMock
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class Product:
"""商品"""
product_id: int
name: str
price: float
stock: int
class PaymentGateway:
"""支付网关(外部服务)"""
def charge(self, amount: float, payment_method: str) -> bool:
"""发起支付"""
# 实际环境中会调用外部支付API
pass
def refund(self, transaction_id: str, amount: float) -> bool:
"""退款"""
pass
class InventoryService:
"""库存服务"""
def check_stock(self, product_id: int) -> int:
"""检查库存"""
pass
def reserve_stock(self, product_id: int, quantity: int) -> bool:
"""预留库存"""
pass
def release_stock(self, product_id: int, quantity: int) -> bool:
"""释放库存"""
pass
class OrderService:
"""订单服务"""
def __init__(self, payment_gateway: PaymentGateway, inventory_service: InventoryService):
self.payment_gateway = payment_gateway
self.inventory_service = inventory_service
self.orders: List[dict] = []
self._order_id_counter = 1
def create_order(self, user_id: int, items: List[dict], payment_method: str = "credit_card") -> dict:
"""
创建订单
流程:
1. 检查库存
2. 预留库存
3. 发起支付
4. 创建订单记录
"""
total_amount = sum(item["price"] * item["quantity"] for item in items)
# 检查所有商品的库存
for item in items:
available = self.inventory_service.check_stock(item["product_id"])
if available < item["quantity"]:
raise ValueError(f"Insufficient stock for product {item['product_id']}")
# 预留库存
for item in items:
if not self.inventory_service.reserve_stock(item["product_id"], item["quantity"]):
raise RuntimeError(f"Failed to reserve stock for product {item['product_id']}")
# 发起支付
payment_success = self.payment_gateway.charge(total_amount, payment_method)
if not payment_success:
# 释放已预留的库存
for item in items:
self.inventory_service.release_stock(item["product_id"], item["quantity"])
raise RuntimeError("Payment failed")
# 创建订单
order = {
"order_id": self._order_id_counter,
"user_id": user_id,
"items": items,
"total_amount": total_amount,
"payment_method": payment_method,
"status": "created",
"created_at": datetime.now().isoformat()
}
self.orders.append(order)
self._order_id_counter += 1
return order
def cancel_order(self, order_id: int) -> bool:
"""取消订单"""
order = next((o for o in self.orders if o["order_id"] == order_id), None)
if not order:
raise ValueError(f"Order {order_id} not found")
if order["status"] in ("shipped", "delivered", "cancelled"):
raise ValueError(f"Cannot cancel order with status: {order['status']}")
# 释放库存
for item in order["items"]:
self.inventory_service.release_stock(item["product_id"], item["quantity"])
# 退款
self.payment_gateway.refund(f"txn_{order_id}", order["total_amount"])
order["status"] = "cancelled"
return True
class TestOrderServiceIntegration:
"""订单服务集成测试"""
@pytest.fixture
def mock_payment_gateway(self):
"""模拟支付网关"""
gateway = Mock(spec=PaymentGateway)
gateway.charge.return_value = True
gateway.refund.return_value = True
return gateway
@pytest.fixture
def mock_inventory_service(self):
"""模拟库存服务"""
inventory = Mock(spec=InventoryService)
inventory.check_stock.return_value = 100 # 充足库存
inventory.reserve_stock.return_value = True
inventory.release_stock.return_value = True
return inventory
@pytest.fixture
def order_service(self, mock_payment_gateway, mock_inventory_service):
"""创建订单服务实例"""
return OrderService(mock_payment_gateway, mock_inventory_service)
def test_create_order_success(self, order_service, mock_payment_gateway, mock_inventory_service):
"""测试成功创建订单"""
items = [
{"product_id": 1, "name": "Laptop", "price": 5000, "quantity": 1},
{"product_id": 2, "name": "Mouse", "price": 100, "quantity": 2},
]
order = order_service.create_order(user_id=100, items=items)
assert order is not None
assert order["user_id"] == 100
assert order["total_amount"] == 5200
assert order["status"] == "created"
# 验证支付网关被调用
mock_payment_gateway.charge.assert_called_once_with(5200, "credit_card")
# 验证库存服务被调用
assert mock_inventory_service.reserve_stock.call_count == 2
def test_create_order_insufficient_stock(self, order_service, mock_inventory_service):
"""测试库存不足时创建订单失败"""
mock_inventory_service.check_stock.return_value = 0 # 库存为0
items = [{"product_id": 1, "name": "Laptop", "price": 5000, "quantity": 1}]
with pytest.raises(ValueError) as exc:
order_service.create_order(user_id=100, items=items)
assert "Insufficient stock" in str(exc.value)
def test_create_order_payment_failure(self, order_service, mock_payment_gateway, mock_inventory_service):
"""测试支付失败时订单创建失败"""
mock_payment_gateway.charge.return_value = False # 支付失败
items = [{"product_id": 1, "name": "Laptop", "price": 5000, "quantity": 1}]
with pytest.raises(RuntimeError) as exc:
order_service.create_order(user_id=100, items=items)
assert "Payment failed" in str(exc.value)
# 验证库存被释放
mock_inventory_service.release_stock.assert_called_once()
def test_cancel_order_success(self, order_service, mock_payment_gateway, mock_inventory_service):
"""测试成功取消订单"""
# 先创建订单
items = [{"product_id": 1, "name": "Laptop", "price": 5000, "quantity": 1}]
order = order_service.create_order(user_id=100, items=items)
# 取消订单
result = order_service.cancel_order(order["order_id"])
assert result is True
assert order["status"] == "cancelled"
# 验证退款
mock_payment_gateway.refund.assert_called_once()
# 验证释放库存
mock_inventory_service.release_stock.assert_called()
def test_cancel_shipped_order_fails(self, order_service):
"""测试取消已发货订单失败"""
items = [{"product_id": 1, "name": "Laptop", "price": 5000, "quantity": 1}]
order = order_service.create_order(user_id=100, items=items)
order["status"] = "shipped" # 模拟已发货
with pytest.raises(ValueError) as exc:
order_service.cancel_order(order["order_id"])
assert "Cannot cancel" in str(exc.value)
四、端到端测试实战
4.1 API端到端测试
python
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, Mock
import sys
import os
# 添加应用路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 假设这是我们的FastAPI应用
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI(title="E2E Test App")
# 模型定义
class Item(BaseModel):
id: Optional[int] = None
name: str
description: Optional[str] = None
price: float
quantity: int
class OrderCreate(BaseModel):
user_id: int
items: List[Item]
class OrderResponse(BaseModel):
order_id: int
user_id: int
items: List[Item]
total: float
status: str
# 内存存储
items_db = {}
orders_db = {}
order_id_counter = 1
# 路由
@app.get("/")
async def root():
return {"message": "E2E Test API"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@app.post("/items/", status_code=201)
async def create_item(item: Item):
item_id = len(items_db) + 1
item_dict = item.dict()
item_dict["id"] = item_id
items_db[item_id] = item_dict
return item_dict
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
@app.get("/items/")
async def list_items(skip: int = 0, limit: int = 10):
return list(items_db.values())[skip:skip+limit]
@app.post("/orders/", status_code=201)
async def create_order(order: OrderCreate):
global order_id_counter
# 验证所有商品存在
for item in order.items:
if item.id is None or item.id not in items_db:
raise HTTPException(status_code=400, detail=f"Item {item.id} not found")
total = sum(item.price * item.quantity for item in order.items)
order_dict = {
"order_id": order_id_counter,
"user_id": order.user_id,
"items": [item.dict() for item in order.items],
"total": total,
"status": "pending"
}
orders_db[order_id_counter] = order_dict
order_id_counter += 1
return order_dict
@app.get("/orders/{order_id}")
async def get_order(order_id: int):
if order_id not in orders_db:
raise HTTPException(status_code=404, detail="Order not found")
return orders_db[order_id]
# E2E测试
@pytest.fixture
def client():
"""测试客户端"""
return TestClient(app)
@pytest.fixture(autouse=True)
def clear_db():
"""每个测试前清空数据库"""
global items_db, orders_db, order_id_counter
items_db.clear()
orders_db.clear()
order_id_counter = 1
class TestHealthEndpoint:
"""健康检查端点测试"""
def test_health_check(self, client):
"""测试健康检查"""
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
class TestItemEndpoints:
"""商品端点E2E测试"""
def test_create_and_get_item(self, client):
"""测试创建和获取商品"""
# 创建商品
item_data = {
"name": "Laptop",
"description": "Gaming laptop",
"price": 999.99,
"quantity": 10
}
response = client.post("/items/", json=item_data)
assert response.status_code == 201
created_item = response.json()
assert created_item["name"] == "Laptop"
assert created_item["id"] is not None
# 获取商品
item_id = created_item["id"]
response = client.get(f"/items/{item_id}")
assert response.status_code == 200
assert response.json()["name"] == "Laptop"
def test_get_nonexistent_item(self, client):
"""测试获取不存在的商品"""
response = client.get("/items/999")
assert response.status_code == 404
def test_list_items(self, client):
"""测试列出商品"""
# 创建多个商品
for i in range(5):
client.post("/items/", json={
"name": f"Item {i}",
"price": 10.0 * (i + 1),
"quantity": i + 1
})
# 列出商品
response = client.get("/items/")
assert response.status_code == 200
items = response.json()
assert len(items) == 5
# 测试分页
response = client.get("/items/?skip=2&limit=2")
assert len(response.json()) == 2
class TestOrderEndpoints:
"""订单端点E2E测试"""
def test_create_and_get_order(self, client):
"""测试创建和获取订单"""
# 先创建商品
item_response = client.post("/items/", json={
"name": "Mouse",
"price": 29.99,
"quantity": 10
})
item_id = item_response.json()["id"]
# 创建订单
order_data = {
"user_id": 1,
"items": [
{"id": item_id, "name": "Mouse", "price": 29.99, "quantity": 2}
]
}
response = client.post("/orders/", json=order_data)
assert response.status_code == 201
order = response.json()
assert order["user_id"] == 1
assert order["total"] == 59.98
assert order["status"] == "pending"
# 获取订单
order_id = order["order_id"]
response = client.get(f"/orders/{order_id}")
assert response.status_code == 200
assert response.json()["total"] == 59.98
def test_create_order_with_invalid_item(self, client):
"""测试使用无效商品创建订单"""
order_data = {
"user_id": 1,
"items": [
{"id": 999, "name": "Non-existent", "price": 100, "quantity": 1}
]
}
response = client.post("/orders/", json=order_data)
assert response.status_code == 400
assert "not found" in response.json()["detail"]
4.2 使用Selenium进行浏览器自动化测试
python
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="module")
def browser():
"""浏览器fixture"""
chrome_options = Options()
chrome_options.add_argument("--headless") # 无头模式
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=chrome_options)
yield driver
driver.quit()
class TestWebInterface:
"""Web界面E2E测试"""
def test_page_loads(self, browser):
"""测试页面加载"""
browser.get("https://example.com")
title = browser.title
assert "Example" in title
def test_login_flow(self, browser):
"""测试登录流程"""
browser.get("https://example.com/login")
# 填写登录表单
username_input = browser.find_element(By.NAME, "username")
password_input = browser.find_element(By.NAME, "password")
submit_button = browser.find_element(By.TYPE, "submit")
username_input.send_keys("testuser")
password_input.send_keys("password123")
submit_button.click()
# 等待登录成功
WebDriverWait(browser, 10).until(
EC.url_contains("/dashboard")
)
assert "/dashboard" in browser.current_url
def test_navigation_menu(self, browser):
"""测试导航菜单"""
browser.get("https://example.com/dashboard")
# 点击各个菜单项
menu_items = browser.find_elements(By.CSS_SELECTOR, ".nav-menu li")
assert len(menu_items) > 0
for item in menu_items[:3]: # 测试前三个
item.click()
WebDriverWait(browser, 5).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
五、测试覆盖率与Mock
5.1 测试覆盖率
bash
# 使用pytest-cov计算测试覆盖率
pytest --cov=src --cov-report=html tests/
python
# .coveragerc 配置文件
[run]
source = src
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
precision = 2
show_missing = True
5.2 Mock对象
python
import pytest
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime
class UserService:
"""用户服务"""
def __init__(self, email_client, user_repository):
self.email_client = email_client
self.user_repository = user_repository
def register_user(self, username: str, email: str, password: str) -> dict:
"""注册用户"""
# 检查邮箱是否已存在
existing = self.user_repository.find_by_email(email)
if existing:
raise ValueError("Email already registered")
# 创建用户
user = self.user_repository.create(username, email, password)
# 发送欢迎邮件
self.email_client.send(
to=email,
subject="Welcome!",
body=f"Hello {username}, welcome to our platform!"
)
return user
class TestUserServiceWithMocks:
"""使用Mock的用户服务测试"""
@pytest.fixture
def mock_email_client(self):
"""模拟邮件客户端"""
return Mock()
@pytest.fixture
def mock_user_repository(self):
"""模拟用户仓储"""
return Mock()
@pytest.fixture
def user_service(self, mock_email_client, mock_user_repository):
"""创建用户服务实例"""
return UserService(mock_email_client, mock_user_repository)
def test_register_user_success(self, user_service, mock_email_client, mock_user_repository):
"""测试成功注册用户"""
mock_user_repository.find_by_email.return_value = None
mock_user_repository.create.return_value = {
"id": 1,
"username": "testuser",
"email": "test@example.com"
}
result = user_service.register_user(
username="testuser",
email="test@example.com",
password="password123"
)
assert result["username"] == "testuser"
# 验证邮件发送
mock_email_client.send.assert_called_once()
call_args = mock_email_client.send.call_args
assert call_args.kwargs["to"] == "test@example.com"
assert "Welcome" in call_args.kwargs["subject"]
def test_register_duplicate_email(self, user_service, mock_user_repository):
"""测试重复邮箱注册失败"""
mock_user_repository.find_by_email.return_value = {"id": 1, "email": "existing@example.com"}
with pytest.raises(ValueError) as exc:
user_service.register_user(
username="testuser",
email="existing@example.com",
password="password123"
)
assert "already registered" in str(exc.value)
# 验证用户未被创建
mock_user_repository.create.assert_not_called()
def test_register_sends_welcome_email(self, user_service, mock_email_client, mock_user_repository):
"""测试注册时发送欢迎邮件"""
mock_user_repository.find_by_email.return_value = None
mock_user_repository.create.return_value = {"id": 1, "username": "newuser", "email": "new@example.com"}
user_service.register_user("newuser", "new@example.com", "password")
# 验证邮件发送被调用
mock_email_client.send.assert_called_once_with(
to="new@example.com",
subject="Welcome!",
body="Hello newuser, welcome to our platform!"
)
六、性能测试
6.1 性能基准测试
python
import pytest
import time
import asyncio
from typing import List
def measure_time(func):
"""测量函数执行时间"""
start = time.perf_counter()
result = func()
elapsed = time.perf_counter() - start
return result, elapsed
class TestPerformance:
"""性能测试"""
def test_list_operations_performance(self):
"""测试列表操作性能"""
# 创建测试数据
data = list(range(10000))
# 测试append操作
def append_1000():
lst = []
for i in range(1000):
lst.append(i)
return lst
_, append_time = measure_time(append_1000)
assert append_time < 0.1, f"append too slow: {append_time}s"
# 测试查找操作
def find_last():
return 9999 in data
_, find_time = measure_time(find_last)
assert find_time < 0.01, f"find too slow: {find_time}s"
@pytest.mark.asyncio
async def test_async_performance(self):
"""测试异步操作性能"""
async def slow_operation():
await asyncio.sleep(0.1)
return "done"
start = time.perf_counter()
# 并发执行10个操作
results = await asyncio.gather(*[slow_operation() for _ in range(10)])
elapsed = time.perf_counter() - start
# 10个0.1s的操作,如果是串行需要1s,但并发应该只需要0.1s左右
assert elapsed < 0.3, f"async too slow: {elapsed}s"
assert len(results) == 10
# 使用pytest-benchmark进行基准测试
def pytest_benchmark_insertion_sort(n):
"""插入排序"""
result = []
for i in range(n):
j = i - 1
while j >= 0 and result[j] > i:
result[j + 1] = result[j]
j -= 1
result[j + 1] = i
return result
class TestBenchmark:
"""基准测试"""
def test_sorting_benchmark(self, benchmark):
"""排序算法基准测试"""
result = benchmark(python_sort, list(range(1000)))
assert len(result) == 1000
def test_search_benchmark(self, benchmark):
"""搜索算法基准测试"""
data = list(range(10000))
result = benchmark(data.index, 9999)
assert result == 9999
def python_sort(data):
"""Python内置排序"""
return sorted(data)
七、总结
本文全面介绍了Python自动化测试的各个层面:
- 测试基础理论:理解测试金字塔和TDD开发方法
- 单元测试:使用unittest和pytest框架编写隔离的单元测试
- 集成测试:测试多个组件之间的交互,使用Mock隔离外部依赖
- 端到端测试:从用户视角测试完整的功能流程
- 测试覆盖率:使用pytest-cov测量代码测试覆盖程度
- Mock对象:使用Mock技术隔离外部依赖,专注于被测代码
- 性能测试:确保代码性能满足要求
完善的自动化测试体系是保证软件质量的关键。建议在日常开发中:
- 遵循TDD原则,先写测试再写代码
- 保持测试快速、独立、可重复
- 重视测试覆盖率,但不要追求100%而是追求有效覆盖
- 定期运行完整测试套件,及时发现问题