简体   繁体   English

Python中的单元测试有最小的样式吗?

[英]Is there a minimal style for unittests in Python?

I'm wondering what techniques people use for simplifying the 'size' of code used for unit testing. 我想知道人们使用什么技术来简化用于单元测试的代码的“大小”。 For example I was trying to marshal an object of the class and testing the marshal'ed object (but this presumes marshal is working correctly). 例如,我试图封送该类的对象并测试封送的对象(但这假定封送工作正常)。

Consider the class 考虑上课

import unittest
class Nums(object):
    def __init__(self, n1_, n2_, n3_):
        self.n1, self.n2, self.n3 = n1_, n2_, n3_
def marshal(self):
    return "n1 %g, n2 %g, n3 %g"%(self.n1,self.n2,self.n3)

and then the marshaling based, list based, and normal tests 然后基于封送处理,基于列表和普通测试

class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nu = Nums(10,20,30)
    def test_init1(self):
        self.assertEquals(self.nu.marshal(),"n1 %g, n2 %g, n3 %g"%(10,20,30))
    def test_init2(self):
        self.assertEquals([self.nu.n1,self.nu.n2,self.nu.n3],[10,21,31])
    def test_init3(self):
        self.assertEquals(self.nu.n1,10)
        self.assertEquals(self.nu.n2,21)
        self.assertEquals(self.nu.n3,31)

which give the following errors (since, 20!=21 and 30!=31, my test has a bad initialization or the test conditions are wrong) 给出以下错误(因为20!= 21和30!= 31,我的测试初始化​​错误或测试条件错误)

AssertionError: 'n1 10, n2 20, n3 30' != 'n1 10, n2 21, n3 31'

AssertionError: [10, 20, 30] != [10, 21, 31]

AssertionError: 20 != 21

The first and second error messages are difficult to understand (since you have to parse the string or list). 第一和第二条错误消息很难理解(因为您必须解析字符串或列表)。 However, the 3rd technique rapidly explodes in the amount of code used to test complex objects. 但是,第三种技术迅速激增了用于测试复杂对象的代码量。

Is there a better way to simplify unit tests without creating difficult error messages? 有没有更好的方法来简化单元测试而不产生困难的错误消息? And, without depending on the veracity of a marshal function? 而且,是否不依赖元帅职能的准确性?

[changed test_marshal to marshal ] [将test_marshal更改为marshal ]

I echo the comments above that you should not have test methods on the actual class you are testing. 我回应上面的评论,即您不应在要测试的实际类上使用测试方法。 Functions like test_marshal should be placed elsewhere (assuming that they do exist for testing and not for general usage), typically in your unit test files. test_marshal这样的test_marshal应该放置在其他地方(假设它们确实存在用于测试而不是一般用途),通常在单元测试文件中。 However, setting that aside for the moment, I'd do something like this 但是,暂时将其搁置一旁,我会做这样的事情

import unittest

class Nums(object):
    FORMAT = "n1 %g, n2 %g, n3 %g"  # make this a variable for easy testing

    def __init__(self, n1, n2, n3):
        self.n1, self.n2, self.n3 = n1, n2, n3  # no underscores necessary

    def test_marshal(self):
        return self.FORMAT % (self.n1, self.n2, self.n3)


class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nums = [10, 20, 30]    # make a param list variable to avoid duplication
        self.nu = Nums(*self.nums)  # Python "apply" syntax
        self.nu_nums = [self.nu.n1, self.nu.n2, self.nu.n3]  # we'll use this repeatedly

    def test_init1(self):
        self.assertEquals(self.nu.test_marshal(), self.nu.FORMAT % self.nums )

    def test_init2(self):
        self.assertEquals(self.nums, self.nu_nums)

    def test_init3(self):
        for reference, test in zip(self.nums, self.nu_nums):
            self.assertEquals(reference, test)

See http://docs.python.org/library/functions.html#apply for an explanation of the apply syntax used above. 有关上面使用的Apply语法的说明,请参见http://docs.python.org/library/functions.html#apply

By putting the things you're testing into variables, you can avoid duplication of code, which seems to be your primary concern. 通过将要测试的内容放入变量中,可以避免重复代码,这似乎是您的主要关注点。

As for the error messages being confusing, I guess it depends on how much detail you feel you need. 至于错误消息令人困惑,我想这取决于您觉得需要多少细节。 Personally, the fact that my unit tests give me the line of code and values that were expected and not present tends to make it fairly clear what went wrong. 就我个人而言,我的单元测试为我提供了预期的代码值和不存在的值,这实际上使我很清楚出了什么问题。 However, if you REALLY wanted something that told you specifically which field was incorrect, rather that just the values that didn't match AND you wanted to avoid code duplication, you could write something like this: 但是,如果您确实想要一些可以明确告诉您哪个字段不正确的信息,而不是只是值不匹配并且您想要避免代码重复,则可以编写如下代码:

class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nums = {"n1": 10, "n2": 20, "n3": 30}  # use a dict, not a list
        self.nu = Nums(**self.nums)                 # same Python "apply" syntax

    # test_init1 and test_init2 omitted for space

    def test_init3(self):
        for attr,val in self.nums.items():
            self.assertEqual([attr, val], [attr, getattr(self.nu, val)])

If you ever did have non-matching values, you'd now get errors that look like 如果您确实有不匹配的值,那么现在会出现类似以下的错误

AssertionError: ["n1", 10] != ["n1", 11]

and thus you'd know at a glance which field didn't match, instead of having to reason it out based on what the values were. 因此您一眼就能知道哪个字段不匹配,而不必根据值是什么来推断出来。 This approach still preserves the code expansion problem, since test_init3 will stay the same size no matter how many parameters you add to your Nums class. 这种方法仍然保留了代码扩展问题,因为无论您向Nums类添加多少参数,test_init3的大小都将保持不变。

Note that this use of getattr requires that your __init__ parameters have the same name as the fields in the num class, eg they must be named n1 rather than n1_ , etc. An alternative approach would be to use the __dict__ attribute, as described here . 请注意,此getattr的使用要求您的__init__参数与num类中的字段具有相同的名称,例如,它们必须命名为n1而不是n1_ ,等等。另一种方法是使用__dict__属性,如此处所述

For testing initialization, I recommend not testing via calling the marshal() function. 对于测试初始化​​,建议不要通过调用marshal()函数进行测试。 Not only do you then have to parse out which initializer failed, you also have to figure out whether it's your initialization that's failing or the marshal function itself. 然后,您不仅必须解析出哪个初始化失败,还必须弄清楚是初始化失败还是编组函数本身。 The "minimal style" for unit tests I would recommend is to narrow down the focus of what you're testing at any test point as much as is feasible. 我建议单元测试的“最小样式”是在可行的情况下尽可能缩小您在任何测试点进行测试的重点。

If I really had to test the initialization of a whole bunch of fields, I might refactor much the same way as Eli: 如果我真的必须测试整个字段的初始化,则可以使用与Eli相同的方式进行重构:

class MyTestCase(unittest.TestCase):
    def assertFieldsEqual(self, obj, expectedFieldValDict):
        for fieldName, fieldVal in expectedFieldValDict.items():
            self.assertFieldEqual(obj, fieldName, fieldVal)
    def assertFieldEqual(self, obj, fieldName, expectedFieldVal):
        actualFieldVal = getattr(obj, fieldName)
        if expectedFieldVal != actualFieldVal:
            msg = "Field %r doesn't match: expected %r, actual %r" % \
                (fieldName, expectedFieldVal, actualFieldVal)
            self.fail(msg)

class NumsTests(MyTestCase):
    def setUp(self):
        self.initFields = {'n1': 10, 'n2': 20, 'n3': 30}
        self.nums = Nums(**initFields)
    def test_init(self):
        self.assertFieldsEqual(self.nums, self.initFields)

"Good grief," I can hear you say, "that's a lot of code!" 我听到你说:“真是太可惜了,其中有很多代码!” Well yeah, but the differences are: 是的,但是区别在于:

  • assertFieldsEqual and assertFieldEqual are reusable functions that have been abstracted to a common test case class which your other test cases can reuse. assertFieldsEqualassertFieldEqual是可重用的函数,这些函数已抽象为公共测试用例类,您的其他测试用例可以重用这些类。
  • The failure message describes exactly what's going on. 故障消息准确描述了正在发生的事情。

When it comes time to test your marshal function, you can simply do this: 当需要测试元帅功能时,只需执行以下操作:

def test_marshal(self):
    expected = '...'
    self.assertEqual(expected, self.nums.marshal())

When comparing strings, though, I prefer a method that tells me exactly where the strings diverge. 但是,在比较字符串时,我更喜欢一种方法,该方法可以准确地告诉我字符串在哪里发散。 For multiline strings, there's now a method for that in Python 2.7, but I've rolled or cribbed my own methods for this purpose in the past. 对于多行字符串,Python 2.7中现在有一个用于该方法的方法,但是过去我为此目的滚动或编写了自己的方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM