阅读完前文并行计算的小伙伴们,可以去把最强电脑配置起来了 :)
多多教Python:Python 基本功: 14. 多核并行计算
因为 Python 计算力的拓展性 (Scalability)很强,所以少量代码就可以做大型计算。但是只要有代码,就会有 Bug。我们不希望用上所有的算力去运行一段错误的代码,这样获得的结果也没有意义,所以这篇我们来过一下 Python 的测试方案。
原本打算介绍 Python 的单元测试 Unittest 模块,但是这个模块作者本人不是很喜欢用,原因会在文章中分析出来。但是作者还是一个会去写测试的人,而这篇要讲的仿真测试 Mocking Test,是作者更加常用的,而且自认为是更加优秀的测试方法。
教程需求:
- Mac OS (Windows, Linux 会略有不同)
- 安装了 Python 3.0 版本以上, PyCharm
- 阅读了
多多教Python:Python 基本功: 7. 介绍函数
多多教Python:Python 基本功: 10. 面对对象-类 Class
单元测试 Unittest
单元测试就是把一个需要被测试的代码整体拆分成一个个逻辑单元进行测试。Python 有一个自带的单元测试库 Unittest, 通过调用这个库,你可以用更少量的代码做最系统性的单元测试。在 多多教Python:Python 基本功: 7. 介绍函数 中,我们写了一个可以汇总某资产日交易价格的函数 report_daily_price,现在我们尝试对这个函数进行单元测试:
import unittest
# report_daily_price 函数见后文
class TestReportDailyPrice(unittest.TestCase):
"""
继承了单元测试,用于测试 report_daily_price 函数的类
"""
def setUp(self) -> None:
# 清空测试环境里之前生成的报告
os.system("rm *_report_*.json")
return
def test_abc_20191213(self):
"""
ABC 价格文件,20191213 单日测试
"""
abc_file = 'abc_date_price.txt'
report_date = "20191213"
# 调用被测试的函数
abc_dict = report_daily_price(ticker='ABC', input_file=abc_file, report_date=report_date)
# 确认返回的是字典格式
self.assertIsInstance(abc_dict, dict)
with open("ABC_report_20191213.json", 'r') as json_to_read:
abc_dict_read = json.load(json_to_read)
# 确认报告的抬头是 AB Price Report
self.assertEqual(abc_dict_read['Title'], "ABC Price Report")
# 确认没有没有日期的文件被生成
self.assertRaises(FileNotFoundError, open, "ABC_report_.json")
# 测试完成,返回
return
def tearDown(self) -> None:
# 清空测试中生成的报告
os.system("rm *_report_*.json")
return
if __name__ == '__main__':
unittest.main()
- unittest 模块: Python 中做单元测试的模块。
- report_daily_price 函数: 在 多多教Python:Python 基本功: 7. 介绍函数 中编写的每日金融数据汇报函数,这里可以从文件中调用,或者之间在后文中拷贝进来。
- TestReportDailyPrice 类: 这个类继承了 unittest.TestCase 类,所有的单元测试可以在这个类里面进行实现。
- setUp 函数: 这个函数覆盖了 unittest.TestCase 类的默认函数,在单元测试进行之前做环境的部署。这里简单的把之前生成的 .json 文件删除,为了避免不同测试之间的干扰。
- test_abc_20191213 函数: 任何 test_ 开头的函数都是属于单元测试函数,这个函数测试的是2019年12月13日生成的 ABC 资产汇报文件。
- self.assertIsInstance 函数: 是属于 unittest.TestCase 的继承函数,对目标 Object 的类进行确认。
- self.assertEqual 函数: 同样属于 unittest.TestCase 的继承函数,对目标 Object 的数值进行确认。
- self.assertRaises 函数: 属于 unittest.TestCase 的继承函数,对方法 Method 报错种类进行确认。
- tearDown 函数: 这个函数覆盖了 unittest.TestCase 类的默认函数,在单元测试全部完成之后,用来清理测试结果。这里简单的把测试中生成的所有 .json 文件清除,为了防止和之后的测试造成冲突。
- unittest.main() 函数: 调用这个函数之后,单元测试就开始了。
这是 report_daily_price 函数的实现,在小伙伴写单元测试的时候,完全可以按照自己的需求写一个需要被测试的函数或者类:
# report_daily_price.py
from datetime import datetime
import json
def report_daily_price(ticker, input_file, report_date=""):
"""
汇报每日价格
:param ticker: 股票符号
:param input_file: 读取文件
:param report_date: 汇报日期
:return: ret_dict: 回复一个字典数据
"""
ret_dict = dict()
with open(input_file, 'r') as file_to_read:
for line in file_to_read.readlines():
# 逗号是分隔符,Delimiter
date_str, price_str = line.split(',')
# 读取的字符串,转换成对应的数据类型
dt = datetime.strptime(date_str, '%m/%d/%Y')
price = float(price_str)
# 数据存入字典,以之后调用
ret_dict[dt] = price
# 计算高低价格,回报率
max_price = max(ret_dict.values())
min_price = min(ret_dict.values())
try:
ret_pcnt = (ret_dict[datetime(2019, 7, 18)] - ret_dict[datetime(2019, 7, 10)]) / ret_dict[datetime(2019, 7, 18)]
except KeyError as key_error:
print("Key error caught: " + str(key_error))
ret_pcnt = 0
if ret_pcnt == 0:
raise ZeroDivisionError("Return pcnt should not be 0, check calculation")
# 创建报告字典
output_dict = dict()
output_dict['Title'] = ticker + " Price Report"
output_dict['Max Price'] = str(max_price)
output_dict['Min Price'] = str(min_price)
output_dict['Return %'] = "{:.2f}".format(ret_pcnt * 100) + "%"
# 写入文件
report_file = ticker + '_report' + "_" + report_date + '.json'
try:
with open(report_file, 'w') as file_to_write:
json.dump(output_dict, file_to_write)
except IOError as io_error:
print("IO error caught: " + str(io_error))
return ret_dict
在 PyCharm 中点进开始运行,如果全部测试通过,就会显示下面的结果:
Ran 1 test in 0.024s
OK
单元测试的局限性
在上面例子中,我们对一个目标函数,测试了一个指定文件,在一个特定的日期下,属于静态测试。就像下图所示:
简单的静态测试环境
在静态测试环境中,所有的测试准备工作都可以在可预知的情况下事先完成。我们可以自己创造测试数据,并且比较测试结果。在同样的测试数据下,我们会期待相同的测试结果。而现实中很多应用需要在一个动态的测试环境中测试,如下图:
复杂的动态测试环境
动态测试环境比较复杂,数据像多媒体,流数据,数据池需要大量的资源去搭建。结果到头来大量的时间用在搭建测试环境上,测试环境自身的不稳定和不全面会对测试造成反面的影响。
除此之外,作者认为单元测试的短版还有下面几条:
- 单元测试需要把测试逻辑拆开来,分割成单元。这个要求在设计软件的时候事先做好测试安排。
- 单元测试的测试范围有限,哪怕你写的再多也无法保证覆盖复杂的动态生产环境。
- 单元测试全部通过会给你产生一种软件已经无敌的幻觉。
仿真测试 Mocking Test
仿真测试的概念是,与其去创建一个复杂的测试环境,不如在设计函数,类的时候,就加入仿真 Mocking
仿真测试环境
一个仿真的函数,类 拥有和主体一样的核心逻辑,但是在环境交互上做了手脚。比方说我们要测试一个交易软件,而测试一个交易软件是需要在测试环境中搭建一个交易所,用户资金池的,这样做会相当的复杂,并且搭建出来的测试环境也不能代表真实的生产环境。那么我们该如何利用仿真测试,来对交易软件做测试呢?下面看示例:
# class15_mocking_example.py
from collections import defaultdict
class Trader:
def __init__(self, api):
self.api = api
self.positions = defaultdict(int)
return
def trade(self, order):
# 在交易所下单
result = self.place_trade(order)
self.process_result(result)
return
def place_trade(self, order):
# 链接交易所
self.api.connect()
# 在交易所下单
result = self.api.submit_order(order)
return result
def process_result(self, result):
# 解析交易结果
self.positions[result['ticker']] += result['shares']
return
class MockTrader(Trader):
def place_trade(self, order):
# 在不链接交易所的情况下,模拟下单
result = dict()
result['ticker'] = order['ticker']
if order['side'] == 'buy':
result['shares'] = order['shares']
else:
result['shares'] = -order['shares']
return result
if __name__ == '__main__':
trader = MockTrader(api=None)
trader.trade({'ticker': 'BABA', 'side': 'buy', 'shares': 100})
assert(trader.positions['BABA'] == 100)
- Trader 类: 这里我们创建了一个交易员类,用来在交易所下单,并且保存持仓数量。交易员类需要一个和交易所链接的渠道,就是这里我们传入的 API 类。
- trade 函数: 通过调用交易员的 trade 函数,可以把传入的订单 order 参数发送到交易所,随后取得交易所的交易结果,对交易结果进行处理并且保存起来。
- place_trade 函数: 这个函数是负责把订单发送到交易所,是一个 I/O Blocking 阻塞函数,并且需要和交易所的实际链接,这个函数将在下面被继承类覆盖掉,使得在没有交易所链接的情况下模拟下单。
- process_result 函数: 这个函数负责把交易结果处理并且保存在本地。
- MockTrader 类: 这是一个仿真交易员,继承了交易员 Trader 的所有函数,在测试中将被用到。
- 覆盖 place_trade 函数: 这是一个唯一需要和交易所链接的函数,我们把其覆盖掉,对交易所做一个简单的模拟订单处理,并且返回相似的结果数据,这样就不需要改变交易结果处理函数。
- main() 函数: 在 main() 函数中,我们创建了一个仿真交易员,并且和交易所的 API 链接为 None,也就是没有。我们做了一笔买入阿里巴巴100股的交易,然后测试看交易员手上是否有100股。
我们可以看到,利用仿真测试,我们可以用最小量的代码来应对复杂的动态测试环境。在这个例子中,我们仍然需要模拟交易所处理订单的逻辑,但是在之后的教程中 多多教Python 金融 ,我会和大家一起研究如何在复杂多变的金融环境中做最全面的 Python 测试。
小结:
通常单元测试 Unittest 应该和 仿真测试 Mocking Test 结合起来一起用。单元测试适合在静态环境中做逻辑层测试,而仿真测试适合在动态环境做实盘演示。单元测试中有很多 assert* 测试方法在教程中没有讲到,作者认为这些根据需求百度一下就行了,有兴趣的小伙伴在下面的外部链接中可以看到:
Python 的单元测试 Unittest:
更加进阶的自动化测试: