Repository navigation
Expand file tree
/
Copy path4_3_code_quality.py
More file actions
190 lines (146 loc) · 7.12 KB
/
Copy path4_3_code_quality.py
File metadata and controls
190 lines (146 loc) · 7.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
================================================================
第四阶段:开发环境与团队协作
4.3 可读代码、类型注解与自动化测试
================================================================
【课程属性】L1 · 必修 · 预计 120~160 分钟
【前置要求】完成 3.3 HTTP 基础;建议先学习 4.1 Git 与 4.2 依赖管理
【学习目标】
1. 能按 PEP 8 写出容易阅读的名字和结构
2. 会把需求拆成正常、边界和异常测试
3. 会使用标准库 unittest;能继续迁移到 pytest
4. 理解类型注解能做什么、不能做什么
【运行准备与参考】
unittest 和 typing 属于标准库,无需安装。
unittest: https://docs.python.org/zh-cn/3/library/unittest.html
类型注解: https://docs.python.org/zh-cn/3/library/typing.html
可选 pytest:python -m pip install pytest;https://docs.pytest.org/
"""
import unittest
from collections.abc import Callable
# ============================================================
# 第一部分:可读性先于“聪明写法”
# ============================================================
# 常用命名:变量和函数 snake_case,类 CamelCase,常量 UPPER_CASE。
MAX_RETRIES = 3
def calculate_price(price: float, discount: float = 1.0) -> float:
"""返回折后价;价格和折扣范围无效时抛 ValueError。"""
if price < 0:
raise ValueError("价格不能为负")
if discount <= 0 or discount > 1:
raise ValueError("折扣必须在 (0, 1] 范围内")
return round(price * discount, 2)
# 好函数通常有明确名字、单一职责、清楚输入输出和适当错误说明。
# 注释说明“为什么”,函数名和变量名尽量直接说明“做什么”。
# 团队应使用统一格式化与检查配置,不必手工争论每个空格。
def is_snake_case_name(name: str) -> bool:
"""检查本课约定的简化 ASCII snake_case 风格。"""
if name == "_":
return True
if not name or not ("a" <= name[0] <= "z" or name[0] == "_"):
return False
return all(
"a" <= char <= "z" or "0" <= char <= "9" or char == "_"
for char in name
)
print(is_snake_case_name("user_name")) # True
print(is_snake_case_name("userName")) # False
# 字符串也能用 <= 比较范围:"a" <= char <= "z" 表示 ASCII 小写字母。
# 本函数只检查课程约定的风格,不等同于 Python 标识符语法;关键字、
# Unicode 标识符等属于另一个问题。
# ============================================================
# 第二部分:从 assert 到测试用例
# ============================================================
# 最小检查:
assert calculate_price(100, 0.8) == 80.0
assert calculate_price(0) == 0.0
# 一个完整测试通常包含:
# Arrange(准备输入) -> Act(执行) -> Assert(验证结果)。
class CalculatePriceTests(unittest.TestCase):
def test_normal_discount(self):
price = 100
result = calculate_price(price, 0.75)
self.assertEqual(result, 75.0)
def test_default_discount(self):
self.assertEqual(calculate_price(19.9), 19.9)
def test_rounds_to_two_decimals(self):
self.assertEqual(calculate_price(10, 0.333), 3.33)
def test_negative_price_raises(self):
with self.assertRaisesRegex(ValueError, "价格不能为负"):
calculate_price(-1)
def test_invalid_discount_raises(self):
for discount in (0, -0.1, 1.1):
with self.subTest(discount=discount):
with self.assertRaises(ValueError):
calculate_price(100, discount)
# 写测试时,每个 test_ 方法只验证一个清晰行为。测试异常可使用 assertRaises:
class DivideExampleTests(unittest.TestCase):
def test_normal_division(self):
self.assertEqual(10 / 2, 5.0)
def test_zero_raises(self):
with self.assertRaises(ZeroDivisionError):
10 / 0
# 正常路径证明功能能用;边界值验证 0、临界点、空集合;
# 异常路径证明错误输入不会悄悄产出错误结果。
# 测试应快速、可重复、相互独立,尽量不依赖真实网络和当前时间。
# ============================================================
# 第三部分:运行 unittest 与认识 pytest
# ============================================================
# 保存为 test_price.py 后可以运行:
# python -m unittest test_price.py -v
# 自动发现当前目录下 test_*.py:
# python -m unittest discover -v
# pytest 是常见第三方测试框架,普通 assert 写法更简洁:
#
# def test_normal_discount():
# assert calculate_price(100, 0.8) == 80
#
# import pytest
# def test_negative_price():
# with pytest.raises(ValueError):
# calculate_price(-1)
#
# 安装后运行:python -m pytest -q
# 先掌握测试场景与断言思想,再学习 fixture、mock 和覆盖率。
# 覆盖率只表示“哪些代码执行过”,不能证明断言正确,也不宜机械追求某个数字。
# ============================================================
# 第四部分:类型注解
# ============================================================
def find_user(user_id: int) -> dict[str, object] | None:
if user_id == 1:
return {"id": 1, "name": "小林"}
return None
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(number: int) -> int:
return number * 2
print(apply_twice(double, 3))
# list[int] 表示整数列表;str | None 表示字符串或 None;
# Callable[[int], int] 表示“接收 int、返回 int 的函数”。
# 类型注解默认不会在运行时自动拒绝错误类型,它主要帮助读者、IDE 和检查工具。
# mypy、pyright 等工具可在运行前发现一部分类型不一致,但不能替代测试。
# ============================================================
# 第五部分:格式化、静态检查、测试各司其职
# ============================================================
# 工具名称会随项目而异,职责通常分三类:
# formatter:统一排版;linter:发现未使用变量、可疑代码和风格问题;
# type checker:检查类型关系;test runner:执行行为验证。
# 具体项目以 pyproject.toml 和团队文档为准,不需要一次安装所有同类工具。
def run_lesson_tests() -> unittest.result.TestResult:
suite = unittest.defaultTestLoader.loadTestsFromTestCase(CalculatePriceTests)
return unittest.TextTestRunner(verbosity=1).run(suite)
if __name__ == "__main__":
result = run_lesson_tests()
if not result.wasSuccessful():
raise SystemExit(1)
# ============================================================
# 本课检查
# ============================================================
# [ ] 会把需求拆成正常、边界和异常场景
# [ ] 会用 unittest 执行测试并读懂失败信息
# [ ] 知道测试覆盖率不等于测试质量
# [ ] 能写 list、联合类型和 Callable 注解
# [ ] 能区分格式化、静态检查、类型检查与测试
# 课后练习:exercises/4_3_code_quality.py