关于python:嘲弄一个类的构造

Mocking a class's construction

我刚刚开始使用Python的模拟库来帮助编写更简洁和隔离的单元测试。我的情况是我有一个类,它从一个非常复杂的格式中读取数据,我想在这个类上测试一个方法,该方法在一个干净的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class holds_data(object):
    def __init__(self, path):
       """Pulls complicated data from a file, given by 'path'.

        Stores it in a dictionary.
       """

        self.data = {}
        with open(path) as f:
            self.data.update(_parse(f))

    def _parse(self, file):
        # Some hairy parsing code here
        pass

    def x_coords(self):
       """The x coordinates from one part of the data
       """

        return [point[0] for point in self.data['points']]

上面的代码是对我所拥有的代码的简化。实际上,_parse是一种相当重要的方法,我在功能级别对其进行了测试覆盖。

不过,我希望能够在单元测试级别上测试x_coords。如果我通过给这个类一个路径来实例化它,它将违反单元测试的规则,因为:

A test is not a unit test if:

  • It touches the filesystem

因此,我希望能够修补holds_data__init__方法,然后只填写x_coords所需的self.data部分。比如:

1
2
3
4
5
6
from mock import patch
with patch('__main__.holds_data.__init__') as init_mock:
    init_mock.return_value = None
    instance = holds_data()
    instance.data = {'points':[(1,1),(2,2),(3,4)]}
    assert(instance.x_coords == [1,2,3])

上面的代码可以工作,但感觉它是以一种相当迂回的方式进行这个测试。有没有更惯用的方法来修补构造函数,或者这是正确的方法?另外,在我的课上或者测试中,是否有一些代码味道我遗漏了?

编辑:很明显,我的问题是,在初始化期间,我的类进行了大量的数据处理来组织将由类似x_coords的方法呈现的数据。我想知道修补所有这些步骤的最简单方法是什么,而不必提供完整的输入示例。我只想在我控制它使用的数据的情况下测试x_coords的行为。

我的问题是这里是否有代码气味,归根结底就是这个问题:

我敢肯定,如果我重构使x_coords成为一个以holds_data为参数的独立函数,这将更容易。如果"更容易测试==更好的设计"成立,那么这将是一个可行的方法。但是,它需要x_coords函数了解更多关于holds_data的内部内容,这通常是我可以接受的。我应该在哪里做交易?清洁剂代码还是清洁剂测试?


既然您只对测试一种方法感兴趣,那么为什么不模拟整个HoldsData类,并将其固定为x_coords方法呢?

1
2
3
4
>>> mock = MagicMock(data={'points': [(0,1), (2,3), (4,5)]})
>>> mock.x_coords = HoldsData.__dict__['x_coords']
>>> mock.x_coords(mock)
[0, 2, 4]

这样,您就可以完全控制x_coords的输入和输出(副作用或返回值)。

注:在PY3K中,由于没有更多的unbound methods,你可以只做mock.x_coords = HoldsData.x_coords

这也可以在模拟对象的构造函数中完成:

1
MagicMock(data={'points': [(0,1), (2,3), (4,5)]}, x_coords=HoldsData.__dict__['x_coords'])


您基本上遇到了这个问题,原因是:

A test is not a unit test if:

  • It touches the filesystem

如果您希望遵循此规则,您应该修改_parse方法。特别是,它不应该将文件作为输入。_parse的任务是解析数据,但是数据来自何处不是该方法所关心的。

您可以有一个包含相同数据的字符串,然后将这些数据传递给_parse。类似地,数据可以来自数据库或其他完全不同的地方。当_parse只接受数据作为输入时,可以用更简单的方法对该方法进行单元测试。

它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
class HoldsData(object):
    def __init__(self, path):
        self.data = {}
        file_data = self._read_data_from_file(path)
        self.data.update(self._parse(file_data))

    def _read_data_from_file(self, path):
        # read data from file
        return data

    def _parse(self, data):
        # do parsing

当然,干净的代码会导致干净的测试。最好的情况是模拟数据并向_parse提供输入,然后在稍后测试x_coords。如果不可能的话,我会照原样处理。如果您所担心的测试用例中只有六行模拟代码,那么您就没事了。