我用Python解决了一个数据转换问题,但是没有测试! 这是我可能会测试的方法。


在上一篇文章中,我通过Python的Messily思考解决了一个问题。 不过,我没有做过一件非常Python化的事情—我没有编写测试 。 部分原因是这是我一次性编写的快速代码,部分原因是该博客文章已经有点长。
概括地说,问题的核心很简单:从类似结构的数据中转换数据
boop = {“你好”:[1、2、3],“再见”:[4、5、6]}
像这样的结构
- 荒野词–乔·安·哈里斯(Jo Ann Harris)
- 让倒计时开始:推销NYE活动的营销技巧
- 克赖恩·戴维斯(Crean Davis):社交情感学习能否在学校中广为人知?
- 完全合格? 关于学位和工作的想法。
- 我如何学习新东西
哔哔声= [{“ hello”:1,“ bye”:4},{“ hello”:1,“ bye”:5},{“ hello”:1,“ bye”:6},{“ hello”: 2,“再见”:4},{“ hello”:2,“再见”:5},{“ hello”:2,“再见”:6},{“ hello”:3,“再见”:4} ,{“ hello”:3,“再见”:5},{“ hello”:3,“再见”:6}]]
您可以查看我提出的特定解决方案,但是本文不依赖于细节。 现在我们所要做的就是假设有一个函数组合完全完成了转换。
好吧,测试一下。 显而易见的第一个测试是:
def test_combos():
boop =…
哔声=…
断言combos(boop)==哔声
还不错,但仅测试一个非常特殊的数据集 ,可能还应该有一些其他示例……有多少个? 而且,尽管此特定的转换代码没有很多条件,所以可能没有太多边缘情况…… 边缘情况是什么? 如果此代码确实有很多条件呢? 我们何时将进行足够的测试,以确保该代码可以在我们可能抛出的所有错误中起作用,而不仅仅是几个示例?
这就是基于属性的测试的出现,这是我大力提倡的测试技术。 与单元测试或集成测试不同,单元测试或集成测试是与要测试的代码规模有关的名称,基于属性的测试与测试的完成方式有关,并且可以在任何测试规模下使用。 主要区别在于,在基于属性的测试中,您不会选择要使用的特定数据,而您要测试的对象是无论您使用何种数据都适用的属性 。 如果您要同时运行代码,请继续创建一个虚拟环境(我建议使用pipenv,因此请创建一个文件夹,然后在其中运行pipenv install --three )并同时安装pytest和假设( pipenv install --dev pytest hypothesis )。 如果将测试放在与要测试的代码位于同一文件夹中的名称为test_的文件中,然后在其中运行pytest( pipenv run pytest ),它将运行您的测试。
但是什么是基于属性的测试? 首先,这是一个示例(不包含样板):
def test_right_number_products(示例):
lengths = [len(values)for example.values()中的值]
number_expected = reduce(mul,lengths,1)#所有长度的乘积
转换=连击(示例)
断言len(已转换)== number_expected
好的,因此该测试将使用一些示例数据(不必担心,除非知道它将像“ boop”中那样构成数据),然后查找每个值列表有多长时间,然后将它们乘以一起计算数字,并检查combos(example)输出中值的数量与乘积相同。 退一步,这是任何正确实现的combos函数都必须具有的属性,无论提供什么数据,因为包含所有类别值的乘积的词典列表必须具有与乘积相同的词典数(似乎几乎愚蠢地如此详细地解释它,对吧,基于属性的良好测试通常是关于发现那些明显正确的东西的。
但是,示例来自何处? 测试框架即假设得以实现。 为了解释如何做,这是样板:
从给定的假设导入
从hypothesis.strategies导入文本,列表,整数,浮点数,one_of,字典键= text()
值= one_of(integers(),floats())类别=字典(键,列表(值,min_size = 1,max_size = 10,unique = True),min_size = 1,max_size = 4)@给定的(类别)
def test_right_number_products(示例):
…
从底部开始。 @given(categories)告诉假设这是一个为其生成值的测试,它应该使用名为@given(categories)的生成器来确定自变量的值。
名为category的生成器是字典(已导入)的生成器,它使用名为keys的生成器生成键。 它通过获取列表的生成器(导入的)并将其与名为values的生成器一起使用来生成长度为1到10的列表(其中列表中的每个值都是唯一的)来生成值。 制作的词典将具有1至4个键。 (这里的长度限制是因为我们要处理产品,因此如果没有最大值,则运行时会变得很长)。
每次要求输入一个新值(整数或浮点数)时,名为值的生成器都会生成。
名为key的生成器生成文本。
每次假设为测试生成值时,它将生成符合所有这些规则的值。 使用已经提供的假设可以轻松构建此生成器,但是在最坏的情况下,您可以完全定义自己的生成器。
与仅提供一些示例相比,似乎需要做很多工作? 但是写起来还不错。 当然,我们需要一定数量的值,但它们必须是非常具体的值,而不仅仅是任何值。
幸运的是,为需要的值生成生成器会带来很多好处:您可以继续使用它。 另外三个基于属性的测试:
@given(类别)
def test_all_values_unique(示例):
转换=连击(示例)
uniques = Frozenset(tuple(value.items())表示转换后的值)
assert len(transformed)== len(uniques)@given(categories)
def test_all_values_present(示例):
转换=连击(示例)
对于example.items()中的键,Expected_values:
transformd_values = Frozenset(已转换项目的[item [key])
断言Frozenset(expected_values)== transformed_values @ given(类别)
def test_all_keys_present(示例):
转换=连击(示例)
对于转换后的product_dictionary:
断言product_dictionary.keys()== example.keys()
通过这些测试,我们验证了,除了第一个测试中列表中的字典数量正确之外,列表中的每个字典都是唯一的,每个原始键的所有值都作为该键的值显示在输出中(反之亦然),列表中的每个字典都具有与原始字典相同的键。 真正有趣的是,我们开始到达一个地方, 很难想象有任何代码可以通过那些测试并且是不正确的 。 真厉害! 与大量具有大量手写示例值的单元测试相比,少量基于属性的方法更易读 , 可验证 ,并且注入的置信度代码更有效。
在这种情况下,我们可以将所有这些东西塞进一个测试中,因为它们都涉及到首先以一个示例为例,然后计算组合,然后检查某些属性,但这很麻烦,而且这很干净。 思维应该(将会)混乱,但是代码应该干净 。
与关于原始问题的帖子相比,这篇文章较少涉及我如何进行测试,而更多地是关于其结构的阐述,但是我希望它有足够的解释使基于属性的测试开始有意义。 良好的基于属性的测试可以花更多的时间来创建工作,但会带来可观的回报。 我也没有研究好的测试如何驱动开发过程,因为在这种情况下,它们没有作用,但是我认为这对于基于属性的测试的影响较小。 由于专注于通用属性 ,因此他们在编写后的测试中过度涉及实施知识的趋势所遭受的痛苦要小得多。
这是完整的原始代码以及完整的测试: