为什么要测试,测试长什么样
在交易里,bug 不是屏幕上的异常,而是看起来合理的错误数字:盈亏里符号反了、手续费算了两次、信号悄悄用了明天的价格。测试是用已知答案检查代码的小程序,让这类 bug 在你的机器上被发现,而不是在市场上。pytest 会找到名为 test_* 的函数,逐个运行,并报告 assert 失败的那些。测试无需框架就能读懂:准备输入,调用函数,assert 你期望的结果。
# A test is a function with an assert; pytest runs every test_* function it finds
def net_pnl(entry, exit_price, quantity, fee):
return (exit_price - entry) * quantity - fee
def test_net_pnl_long_winner():
assert net_pnl(100.0, 102.0, 50, 1.0) == 99.0
def test_net_pnl_short_loser():
assert net_pnl(100.0, 102.0, -50, 1.0) == -101.0
for test in (test_net_pnl_long_winner, test_net_pnl_short_loser):
test() # pytest does this for you and reports failures
print(test.__name__, "passed")浮点数、错误与大量用例
浮点数是近似的,所以 0.1 + 0.2 == 0.3 为假。比较数字要带容差:pytest.approx(expected) 接受很小的相对误差(默认百万分之一,另有 1e-12 的极小绝对下限),对于接近零、相对容差无用的值,还可以加绝对容差。要检查不良输入被拒绝,pytest.raises(ValueError) 只有在该异常发生时才通过,没有异常(或发生别的异常)就失败。要让一个测试跑很多用例,@pytest.mark.parametrize 提供输入与期望值的表格,每一行单独报告,并使新增用例只需改一行。
# Floats need a tolerance: approx accepts a tiny relative difference, == does not
import pytest
print(0.1 + 0.2 == 0.3) # False
print(0.1 + 0.2 == pytest.approx(0.3)) # True
print(1e-10 == pytest.approx(0.0)) # False near zero a relative tolerance cannot help
print(1e-10 == pytest.approx(0.0, abs=1e-9)) # True a larger absolute tolerance does# raises: the test passes only if the expected exception happens
import pytest
def leverage(gross, equity):
if equity <= 0:
raise ValueError("equity must be positive")
return gross / equity
with pytest.raises(ValueError) as info:
leverage(1000.0, 0.0)
print("raised:", info.value) # raised: equity must be positive
try:
with pytest.raises(ValueError):
leverage(1000.0, 500.0) # no exception: pytest.raises itself fails
except BaseException as outcome:
print(type(outcome).__name__) # Failed# parametrize in miniature: one function, a table of cases, each row judged on its own
def fee(notional, rate=0.001, minimum=1.0):
return max(notional * rate, minimum)
cases = [
((100_000.0,), 100.0),
((500.0,), 1.0), # the minimum fee applies
((2_000.0, 0.002), 4.0),
((0.0,), 1.0),
]
for index, (args, expected) in enumerate(cases):
got = fee(*args)
print(index, "ok" if abs(got - expected) <= 1e-9 * abs(expected) else f"FAIL got {got}")Fixture、种子与测试什么
fixture 是测试按名称请求的共享准备:一小段价格序列、固定种子的随机数生成器、一个临时文件。使用随机数据时,要固定种子(或把它打印出来),这样失败才能复现:一百次才失败一次又无法重放的测试,比没有还糟。测试你关心的行为而不是内部细节:手工算出的已知用例、边界情况(空输入、零、单个元素)、必须始终成立的不变量(概率在 0 到 1 之间,以同价买入再卖出恰好亏掉手续费),以及每修一个 bug 就加一个回归测试。当你能写出对任何输入都成立的规则时,在大量随机输入中寻找反例是一种廉价而有力的测试。
# A seed makes a random test replayable; a property is searched for a counterexample
import random
def find_counterexample(prop, inputs):
for value in inputs:
if not prop(value):
return value
return None
rng = random.Random(42) # fixed seed: the same inputs on every run
returns = [round(rng.uniform(-0.05, 0.05), 4) for _ in range(1000)]
def compound_is_positive_growth(r):
return (1 + r) > 0 # a daily return above -100% keeps the value positive
print(find_counterexample(compound_is_positive_growth, returns)) # None the rule holds for all 1000
print(find_counterexample(lambda r: r > -0.049, returns)) # a return that breaks a wrong rule不会失败的测试什么也证明不了先故意破坏代码,让每个测试都失败一次,这样你才知道它能抓住它要抓的 bug。不要把函数自己的公式抄进测试:如果两者犯同样的错,它们会一致,测试就通过了。要独立或手工算出期望值。并让测试保持快速,不依赖网络和时钟,这样你才会在每次修改前真正去运行它们。
- 测试准备输入、调用函数并
assert结果;pytest 运行每个test_*函数并报告失败。 - 用
pytest.approx比较浮点数(接近零时加绝对容差),用pytest.raises期望错误,用parametrize处理用例表。 - 固定随机种子,测试边界情况、不变量和已修复的 bug,寻找反例,并在信任每个测试之前让它先失败一次。
下一节:Python 代码性能分析,在提速之前先测量时间花在哪里。
实验:评分内容
- 在
is_close中带容差地比较浮点数25 分 - 用
run_cases运行用例表25 分 - 用
raises期望出现错误25 分 - 用
find_counterexample寻找反例25 分