我最近尝试过对自己进行单元测试最佳实践的培训。其中大部分内容都非常有意义,但有些内容经常被忽视和/或解释不清:如何对单元测试装饰功能进行测试?
我们假设我有这段代码:
$('.task-name td:first-child').each(function() {
var str = $(this).text().substring(0, 4);
console.log(str);
});
我显然可以写下面的测试:
def stringify(func):
@wraps(func)
def wrapper(*args):
return str(func(*args))
return wrapper
class A(object):
@stringify
def add_numbers(self, a, b):
"""
Returns the sum of `a` and `b` as a string.
"""
return a + b
这给了我100%的覆盖率:我知道用def test_stringify():
@stringify
def func(x):
return x
assert func(42) == "42"
def test_A_add_numbers():
instance = MagicMock(spec=A)
result = A.add_numbers.__wrapped__(instance, 3, 7)
assert result == 10
修饰的任何函数都将其结果作为字符串获取,并且我知道未修饰的stringify()
函数返回其参数的总和。因此,通过传递性,A.add_numbers()
的修饰版本必须返回其参数的总和,作为字符串。一切似乎都很好!
然而,我对此并不完全满意:我的测试,因为我写的它们仍然可以通过,如果我要使用另一个装饰器(做其他事情,比如将结果乘以2而不是转换为{ {1}})。我的函数A.add_numbers()
不再正确,但测试仍然会通过。不太棒。
我可以测试str
的装饰版本,但是因为我的装饰器已经过单元测试,所以我会推翻它。
感觉我在这里遗漏了一些东西。单元测试装饰函数的好策略是什么?
答案 0 :(得分:1)
测试代码的公共接口。如果你只希望人们打电话给装饰的功能,那么你应该测试什么。如果装饰器也是公共的,那么也要测试它(就像你对 $mother["id"] = get_post_meta(get_the_ID(), "_mother_id", true);
$mother["name"] = get_animal_name($mother["id"]);
$mother["permalink"] = get_the_permalink($mother["id"]);
$mother_mother["id"] = get_mother_id($mother["id"]);
$mother_mother["name"] = get_animal_name($mother_mother["id"]);
$mother_mother["permalink"] = get_the_permalink($mother_mother["id"]);
$mother_father["id"] = get_father_id($mother["id"]);
$mother_father["name"] = get_animal_name($mother_father["id"]);
$mother_father["permalink"] = get_the_permalink($mother_father["id"]);
$father["id"] = get_post_meta(get_the_ID(), "_father_id", true);
$father["name"] = get_animal_name($father["id"]);
$father["permalink"] = get_the_permalink($father["id"]);
$father_mother["id"] = get_mother_id($father["id"]);
$father_mother["name"] = get_animal_name($father_mother["id"]);
$father_mother["permalink"] = get_the_permalink($father_mother["id"]);
$father_father["id"] = get_father_id($father["id"]);
$father_father["name"] = get_animal_name($father_father["id"]);
$father_father["permalink"] = get_the_permalink($father_father["id"]);
所做的那样)。除非人们直接打电话给他们,否则不要测试包装版本。
答案 1 :(得分:1)
单元测试的一个主要好处是允许重构具有一定程度的信心,重构代码继续像以前一样工作。假设您已经开始使用
def add_numbers(a, b):
return str(a + b)
def mult_numbers(a, b):
return str(a * b)
你会有一些测试,比如
def test_add_numbers():
assert add_numbers(3, 5) == "8"
def test_mult_numbers():
assert mult_numbers(3, 5) == "15"
现在,您决定使用stringify
装饰器重构每个函数的公共部分(将输出包装在一个字符串中)。
def stringify(func):
@wraps(func)
def wrapper(*args):
return str(func(*args))
return wrapper
@stringify
def add_numbers(a, b):
return a + b
@stringify
def mult_numbers(a, b):
return a * b
您会注意到此重构后您的原始测试仍然有效。您实施add_numbers
和mult_numbers
的方式并不重要;重要的是它们继续按照定义工作:恢复所需操作的字符串化结果。
您需要编写的唯一剩余测试是验证stringify
执行 it 要执行的操作:返回修饰函数的结果作为字符串,test_stringify
所做的。
您的问题似乎是您希望将展开的函数,装饰器,和包装的函数视为单位。但如果是这种情况,那么你就缺少一个单元测试:实际运行add_wrapper
并测试其输出的测试,而不仅仅是add_wrapper.__wrapped__
。如果你考虑将包装函数作为单元测试或集成测试进行测试并不重要,但无论你怎么称呼它,你都需要编写它,因为正如你所指出的那样,它并不足以只测试未包装的函数和装饰器。
答案 2 :(得分:1)
我最后将装饰师分成两部分。所以没有:
def stringify(func):
@wraps(func)
def wrapper(*args):
return str(func(*args))
return wrapper
我有:
def to_string(value):
return str(value)
def stringify(func):
@wraps(func)
def wrapper(*args):
return to_string(func(*args))
return wrapper
这允许我稍后在测试装饰函数时简单地模拟to_string
。
显然在这个简单的例子中它可能看起来有点过分,但是当在装饰器上使用它实际上做了一些复杂或昂贵的事情(比如打开与DB的连接,或者其他什么)时,能够模拟它是一个非常好的的事情。