Python入门(16)--自动化测试教程
Python之旅第十五站
·
自动化测试教程 🔍
1. 单元测试编写 ✅
1.1 unittest框架介绍
Python的unittest框架提供了编写和运行测试的完整工具集:
import unittest
class TestStringMethods(unittest.TestCase):
def setUp(self):
"""测试前的准备工作"""
self.test_string = "hello world"
def test_upper(self):
"""测试大写转换"""
self.assertEqual(self.test_string.upper(), "HELLO WORLD")
def test_split(self):
"""测试字符串分割"""
self.assertEqual(self.test_string.split(), ['hello', 'world'])
def tearDown(self):
"""测试后的清理工作"""
pass
if __name__ == '__main__':
unittest.main()
1.2 测试用例编写原则
-
FIRST原则
- Fast (快速): 测试应该快速执行
- Independent (独立): 测试之间不应相互依赖
- Repeatable (可重复): 在任何环境下都能得到相同结果
- Self-validating (自验证): 测试应该自动输出结果
- Timely (及时): 测试应该在编写代码之前或同时编写
-
单一职责
- 每个测试用例只测试一个功能点
- 保持测试用例的独立性
- 避免测试用例之间的依赖
-
命名规范
def test_should_create_user_when_valid_data_provided(): # 测试用例名称应该清晰地表达: # - 被测试的场景(should_create_user) # - 触发条件(when_valid_data_provided) pass def test_should_raise_error_when_username_exists(): # 明确指出期望的行为和触发条件 pass -
测试边界条件
def test_user_password_validation(): # 测试最小长度边界 self.assertFalse(validate_password("ab12")) # 5个字符以下 self.assertTrue(validate_password("abc123")) # 6个字符 # 测试最大长度边界 long_password = "a" * 100 self.assertFalse(validate_password(long_password)) # 测试特殊情况 self.assertFalse(validate_password("")) # 空密码 self.assertFalse(validate_password(None)) # None值 -
AAA模式
def test_user_registration(self): # Arrange(准备) user_data = { 'username': 'testuser', 'email': 'test@example.com', 'password': 'secure123' } # Act(执行) response = self.client.post('/register', data=user_data) # Assert(断言) self.assertEqual(response.status_code, 201) self.assertTrue(User.query.filter_by(username='testuser').first())
1.3 Mock对象使用
from unittest.mock import Mock, patch
class TestUserService:
@patch('app.services.database')
def test_get_user(self, mock_db):
# 配置mock对象
mock_db.query.return_value = {
'id': 1,
'name': 'Test User'
}
# 执行测试
user_service = UserService()
result = user_service.get_user(1)
# 验证结果
self.assertEqual(result['name'], 'Test User')
mock_db.query.assert_called_once_with(1)
2. 自动化测试框架 🔄
2.1 pytest框架高级特性
- 强大的夹具系统
import pytest
from datetime import datetime, timedelta
# 作用域为session的夹具,整个测试会话期间只执行一次
@pytest.fixture(scope="session")
def db_connection():
# 建立数据库连接
connection = create_db_connection()
yield connection
# 测试结束后关闭连接
connection.close()
# 作用域为function的夹具,每个测试函数执行前都会重新创建
@pytest.fixture(scope="function")
def temp_user(db_connection):
# 创建临时用户
user = User(username=f"test_user_{datetime.now().timestamp()}")
db_connection.save(user)
yield user
# 测试后清理用户数据
db_connection.delete(user)
# 参数化的夹具
@pytest.fixture(params=[
("standard_user", ["read"]),
("admin_user", ["read", "write", "delete"]),
("guest_user", [])
])
def user_role(request):
return request.param
- 强大的断言机制
def test_advanced_assertions(temp_user):
# 精确匹配
assert temp_user.username.startswith("test_user_")
# 浮点数比较
assert pytest.approx(0.1 + 0.2) == 0.3
# 异常断言
with pytest.raises(ValueError) as excinfo:
temp_user.set_age(-1)
assert "Age cannot be negative" in str(excinfo.value)
# 警告断言
with pytest.warns(DeprecationWarning):
temp_user.old_method()
# 对象属性断言
assert hasattr(temp_user, 'email')
- 跳过和标记测试
@pytest.mark.skip(reason="feature not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(sys.version_info < (3, 9),
reason="requires python3.9 or higher")
def test_new_feature():
pass
@pytest.mark.slow
def test_slow_operation():
# 耗时操作测试
pass
# 在命令行中运行特定标记的测试
# pytest -v -m "not slow"
import pytest
from app.calculator import Calculator
@pytest.fixture
def calculator():
"""测试夹具:创建计算器实例"""
return Calculator()
def test_addition(calculator):
"""测试加法功能"""
assert calculator.add(2, 3) == 5
assert calculator.add(-1, 1) == 0
def test_division(calculator):
"""测试除法功能"""
assert calculator.divide(6, 2) == 3
with pytest.raises(ValueError):
calculator.divide(1, 0)
@pytest.mark.parametrize("input,expected", [
((4, 2), 2),
((9, 3), 3),
((15, 5), 3),
])
def test_division_parameters(calculator, input, expected):
"""参数化测试"""
assert calculator.divide(*input) == expected
2.2 测试覆盖率分析
# coverage配置文件:.coveragerc
[run]
source = app
omit = tests/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
if __name__ == "__main__":
pass
[html]
directory = coverage_html
运行覆盖率测试:
coverage run -m pytest
coverage report
coverage html
3. 持续集成与部署实践 🔁
3.1 CI/CD最佳实践
- 分支策略
main (生产环境)
↑
develop (开发环境)
↑
feature/* (功能分支)
hotfix/* (紧急修复)
release/* (发布准备)
- 构建矩阵
# .github/workflows/test-matrix.yml
jobs:
test:
strategy:
matrix:
python-version: [3.8, 3.9, 3.10]
os: [ubuntu-latest, windows-latest, macos-latest]
database: [sqlite, mysql, postgresql]
exclude:
- os: windows-latest
database: postgresql
runs-on: ${{ matrix.os }}
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test_db
ports:
- 3306:3306
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: password
POSTGRES_DB: test_db
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Run Tests
env:
DB_TYPE: ${{ matrix.database }}
run: |
pytest --cov=app tests/
- 质量控制
# .github/workflows/quality.yml
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install tools
run: |
pip install black flake8 mypy bandit safety
- name: Code formatting
run: black --check .
- name: Linting
run: flake8 .
- name: Type checking
run: mypy .
- name: Security check
run: |
bandit -r .
safety check
### 3.1 CI/CD配置
```yaml
# .github/workflows/python-app.yml
name: Python application
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: |
pytest --cov=app tests/
- name: Upload coverage reports
uses: codecov/codecov-action@v3
3.2 自动化部署流程
-
构建阶段
- 代码检出
- 安装依赖
- 运行测试
- 构建产物
-
部署阶段
- 环境配置
- 数据库迁移
- 服务部署
- 健康检查
4. 实战案例:自动化测试套件 🎯
让我们为博客系统创建完整的测试套件:
import unittest
from flask_testing import TestCase
from app import create_app, db
from app.models import User, Post
class TestConfig:
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
class BlogTestCase(TestCase):
def create_app(self):
"""创建测试应用"""
app = create_app(TestConfig)
return app
def setUp(self):
"""测试准备"""
db.create_all()
self.create_test_user()
def tearDown(self):
"""测试清理"""
db.session.remove()
db.drop_all()
def create_test_user(self):
"""创建测试用户"""
user = User(username='testuser', email='test@example.com')
user.set_password('password123')
db.session.add(user)
db.session.commit()
self.test_user = user
def test_user_registration(self):
"""测试用户注册"""
response = self.client.post('/auth/register', data={
'username': 'newuser',
'email': 'new@example.com',
'password': 'newpass123',
'password2': 'newpass123'
})
self.assertEqual(response.status_code, 302)
user = User.query.filter_by(username='newuser').first()
self.assertIsNotNone(user)
def test_post_creation(self):
"""测试文章创建"""
with self.client:
self.client.post('/auth/login', data={
'username': 'testuser',
'password': 'password123'
})
response = self.client.post('/post/create', data={
'title': 'Test Post',
'content': 'Test Content'
})
self.assertEqual(response.status_code, 302)
post = Post.query.filter_by(title='Test Post').first()
self.assertIsNotNone(post)
self.assertEqual(post.author, self.test_user)
class APITestCase(TestCase):
def test_get_posts(self):
"""测试获取文章列表API"""
response = self.client.get('/api/posts')
self.assertEqual(response.status_code, 200)
self.assertTrue(isinstance(response.json, list))
def test_create_post(self):
"""测试创建文章API"""
response = self.client.post('/api/posts', json={
'title': 'API Test Post',
'content': 'Content from API test'
})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json['title'], 'API Test Post')
def run_tests():
"""运行所有测试"""
unittest.main()
if __name__ == '__main__':
run_tests()
5. 高级测试技术 🚀
5.1 性能测试
import pytest
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5) # 用户等待时间1-5秒
@task(2)
def view_posts(self):
self.client.get("/posts")
@task(1)
def create_post(self):
self.client.post("/posts/create", json={
"title": "New Post",
"content": "Content"
})
# 命令行运行:locust -f locustfile.py --host=http://localhost:8000
5.2 安全性测试
import pytest
from bs4 import BeautifulSoup
import re
class TestSecurity:
def test_xss_prevention(self, client):
"""测试XSS防护"""
malicious_input = "<script>alert('xss')</script>"
response = client.post('/comment/create', data={
'content': malicious_input
})
soup = BeautifulSoup(response.data, 'html.parser')
# 验证脚本标签被转义
assert "<script>" not in str(soup)
def test_sql_injection_prevention(self, client):
"""测试SQL注入防护"""
malicious_input = "' OR '1'='1"
response = client.get(f'/users/search?q={malicious_input}')
assert response.status_code != 500
def test_csrf_protection(self, client):
"""测试CSRF保护"""
# 获取CSRF令牌
response = client.get('/post/create')
soup = BeautifulSoup(response.data, 'html.parser')
csrf_token = soup.find('input', {'name': 'csrf_token'})['value']
# 验证无令牌请求被拒绝
response_without_token = client.post('/post/create', data={
'title': 'Test Post'
})
assert response_without_token.status_code == 400
# 验证有令牌请求被接受
response_with_token = client.post('/post/create', data={
'title': 'Test Post',
'csrf_token': csrf_token
})
assert response_with_token.status_code == 302
5.3 并发测试
import threading
import queue
import pytest
def test_concurrent_access(app, client):
"""测试并发访问"""
num_threads = 10
results = queue.Queue()
def worker():
try:
response = client.post('/api/resource', json={
'name': 'test_resource'
})
results.put(('success', response.status_code))
except Exception as e:
results.put(('error', str(e)))
# 创建多个线程同时访问
threads = []
for _ in range(num_threads):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
# 等待所有线程完成
for t in threads:
t.join()
# 分析结果
success_count = 0
error_count = 0
while not results.empty():
status, _ = results.get()
if status == 'success':
success_count += 1
else:
error_count += 1
# 验证结果
assert success_count == 1 # 只有一个请求应该成功
assert error_count == num_threads - 1 # 其他请求应该失败
测试套件特点:
-
完整的测试覆盖
- 用户认证测试
- 文章管理测试
- API接口测试
- 数据库操作测试
-
测试环境隔离
- 使用内存数据库
- 独立的测试配置
- 每次测试后清理数据
-
持续集成支持
- GitHub Actions集成
- 自动运行测试
- 覆盖率报告生成
-
测试辅助工具
- 测试夹具(Fixtures)
- Mock对象
- 参数化测试
扩展建议:
- 添加性能测试
- 实现端到端测试
- 添加负载测试
- 实现安全性测试
- 添加接口文档测试
- 实现并发测试
- 添加回归测试套件
这个实战案例展示了如何为Web应用构建完整的测试体系,涵盖了自动化测试中的各个重要概念和实践。你可以基于这个框架继续扩展更多测试场景。
如果你觉得这篇文章有帮助,欢迎点赞转发,也期待在评论区看到你的想法和建议!👇
咱们下一期见!
更多推荐




所有评论(0)