【测试】自动化测试实战:从单元测试到端到端测试

【测试】自动化测试实战:从单元测试到端到端测试

前言

软件测试是保证软件质量的重要手段,自动化测试更是现代软件开发流程中不可或缺的环节。随着敏捷开发和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自动化测试的各个层面:

  1. 测试基础理论:理解测试金字塔和TDD开发方法
  2. 单元测试:使用unittest和pytest框架编写隔离的单元测试
  3. 集成测试:测试多个组件之间的交互,使用Mock隔离外部依赖
  4. 端到端测试:从用户视角测试完整的功能流程
  5. 测试覆盖率:使用pytest-cov测量代码测试覆盖程度
  6. Mock对象:使用Mock技术隔离外部依赖,专注于被测代码
  7. 性能测试:确保代码性能满足要求

完善的自动化测试体系是保证软件质量的关键。建议在日常开发中:

  • 遵循TDD原则,先写测试再写代码
  • 保持测试快速、独立、可重复
  • 重视测试覆盖率,但不要追求100%而是追求有效覆盖
  • 定期运行完整测试套件,及时发现问题
相关推荐
han_1 小时前
手把手教你写一个 AI Skill,让 AI 真正学会你的工作流
人工智能·ai编程·claude
蔡俊锋1 小时前
AI广告投放Agent:从Demo到实战的半年进化
人工智能·ai广告投放agent
莱歌数字1 小时前
AR眼镜分区散热方案:让SoC“冷”下来,让光学“稳”住
人工智能·科技·电脑·ar·制造·散热
水木流年追梦1 小时前
大模型入门-Pre-Training、SFT、RLHF
人工智能·深度学习·机器学习
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【57】SAA Admin 前后端技术栈与分层设计详解
java·人工智能·spring
智慧景区与市集主理人1 小时前
商户摊位规范经营!巨有科技助力优化景区商业管控体系
大数据·人工智能·科技
@蔓蔓喜欢你1 小时前
前端状态管理方案:从简单到复杂的演进
人工智能·ai
九皇叔叔1 小时前
Spring-Ai-Alibaba [02] chatclient-demo
java·人工智能·spring·ai
山西茄子1 小时前
DeepStream9.0 inference_builder
人工智能·deepstream