15.5 使用外部定义的期望结果
对于一些应用而言,用户可以表达出描述软件行为的处理规则。在其他情况下,分析师或设计师会将用户的意思转换为软件行为的描述。
在许多情况下,用户如果能够提供具体期望结果的示例会带来很多好处。对于一些面向商务的应用来说,用户可能更倾向于使用excel表格来展示输入和期望输出的示例数据。有了用户提供的数据,可以简化软件的开发。
任何时候,如果可能的话,应当由真实用户提供正确结果的示例数据。创建过程描述或软件规格说明是非常难的。创建具体示例并基于它们来生成软件规格说明会降低复杂度并减少一些困惑。进一步说,它进入了测试用例驱动开发的研发模式。给定了一套测试用例,我们就有了关于完成的具体定义。对软件项目状态的追踪相当于是在问,今天我们有多少个测试用例,它们通过了多少。
一旦给定一个具体示例的电子表格,就需要把每一行转换为一个TestCase实例,然后可以基于这些对象创建一个套件。
对于本章中的例子,我们从一个TestCase子类中加载了测试用例,使用了unittest.defaultTestLoader.loadTestsFromTestCase来查找所有名称以test为起始的所有方法。加载器从每个方法中创建了一个测试对象并将它们合并到一个测试套件中。实际上,由加载器创建的每个对象都是不连续的,通过在类构造函数中使用测试用例名 SomeTestCase("test method name")来完成。传入SomeTestCase init ()函数的参数将作为类定义中的方法名。每一个方法都被单独阐述为一个测试用例。
对这个例子来说,我们将使用其他方式来创建测试用例的实例。定义一个包含一个测试的类,然后将这个TestCase类的多个实例加载到套件中。这时,TestCase类只能被定义一次,而且默认情况下,方法名应该为runTest()。我们不会使用加载器来创建测试对象,将直接基于外部提供的数据来创建它们。
这里看一个需要测试的具体函数。它在第3章“属性访问、特性和修饰符”中介绍过了。
from p1_c03 import RateTimeDistance
这个类在初始化时提前计算了很多属性。这个简单函数的用户以电子表格的形式给我们提供了一些测试用例,它来自我们所提取的CSV文件。有关更多CSV文件的信息,可以参见第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”。我们需要将每一行转换为TestCase。
rate_in,time_in,distance_in,rate_out,time_out,distance_out
2,3,,2,3,6
5,,7,5,1.4,7
,11,13,1.18,11,13
可以使用以下这个测试用例来基于CSV文件中的每行数据创建测试实例:
def floatornone( text ):
if len(text) == 0: return None
return float(text)
class TestRTD( unittest.TestCase ):
def _init( self, rate_in,time_in,distance_in,
rate_out,time_out,distance_out ):
super().__init()
self.args = dict( rate=float_or_none(rate_in),
time=float_or_none(time_in),
distance=float_or_none(distance_in) )
self.result= dict( rate=float_or_none(rate_out),
time=float_or_none(time_out),
distance=float_or_none(distance_out) )
def shortDescription( self ):
return "{0} -> {1}".format(self.args, self.result)
def setUp( self ):
self.rtd= RateTimeDistance( *self.args )
def runTest( self ):
self.assertAlmostEqual( self.rtd.distance, self.rtd.rateself.
rtd.time )
self.assertAlmostEqual( self.rtd.rate, self.result['rate'] )
self.assertAlmostEqual( self.rtd.time, self.result['time'] )
self.assertAlmostEqual( self.rtd.distance, self.
result['distance'] )
float or none()函数是用于处理CSV源数据常见的方式。它将每个单元格的文本转换为float数值或None。
Test _ RTD类做3件事。
- init ()方法将一行电子表格的数据转换为两个字典的值:输入值 self.args和期望的输出值self.result。
- setUp()方法用于创建RateTimeDistance对象并提供输入参数值。
- runTest()方法会基于用户提供的数据来对结果进行检查进而简化输出的验证过程。
还提供了一个shortDescription()方法,返回了对测试的简单总结,这对于调试是有帮助的。可以使用如下代码创建一个套件:
import csv
def suite9():
suite= unittest.TestSuite()
with open("p3_c15_data.csv","r",newline="") as source:
rdr= csv.DictReader( source )
for row in rdr:
suite.addTest( Test_RTD(**row) )
return suite
我们打开了 CSV 文件,然后对每行测试用例进行读取并转换为 dict 对象。如果 CSV列标题与Test RTD. init ()方法的期望值相匹配,那么每行会成为一个测试用例对象并会被加载进入套件中。如果列标题不匹配,会得到一个KeyError异常,需要对电子表格进行修改,与Test RTD类匹配。可以使用如下方式来运行测试。
t= unittest.TextTestRunner()
t.run( suite9() )
输出如下:
..F
======================================================================
FAIL: runTest (main.Test_RTD)
{'rate': None, 'distance': 13.0, 'time': 11.0} -> {'rate': 1.18,
'distance': 13.0, 'time': 11.0}
———————————————————————————————————
Traceback (most recent call last):
File "p3_c15.py", line 504, in runTest
self.assertAlmostEqual( self.rtd.rate, self.result['rate'] )
AssertionError: 1.1818181818181819 != 1.18 within 7 places
———————————————————————————————————
Ran 3 tests in 0.000s
FAILED (failures=1)
用户提供的数据有一个小问题,提供的是一个只被四舍五入到两位的值。要么提供的数据需要被修改为更多位数,要么在测试中需要支持两位数。
依赖用户提供精确的示例数据,可能不太实际。如果用户提供的数据无法足够精确,那么我们的测试就需要根据用户的输入包含更多的四舍五入。电子表格显示的数据看上去都已经是精确的decimal类型了,它们可能已经被四舍五入或者被转换为了只是一个近似值的浮点数,因此这项工作是有挑战的。在多数情况下,可以假设数据全部已经被四舍五入了,而不必通过对电子表格做的逆向工程来试图解析用户的意图。
