Python:测试类的单个方法的最佳方法

时间:2012-07-24 03:38:29

标签: python testing pytest

我有一个如下课程:

class A:
    def __init__(self, arg1, arg2, arg3):
        self.a=arg1
        self.b=arg2
        self.c=arg3
        # ...
        self.x=do_something(arg1, arg2, arg3)
        self.y=do_something(arg1, arg2, arg3)

        self.m = self.func1(self.x)
        self.n = self.func2(self.y)
        # ...

    def func1(self, arg):
        # do something here

    def func2(self, arg):
        # do something here

如您所见,初始化类需要提供arg1,arg2和arg3。但是,测试func1和func2并不直接需要这样的输入,而是它只是一个输入/输出逻辑。

在我的测试中,我当然可以以常规方式实例化和初始化测试对象,然后单独测试func1和func2。但初始化需要输入arg1 arg2,arg3,这与测试func1和func2无关。

因此,我想单独测试func1和func2,而不是先调用__init__。所以我有以下两个问题:

  1. 设计此类测试的最佳方法是什么? (最好是在py.test中)
  2. 我想在没有调用__init__的情况下测试func1和func2 。我从here读到A.__new__()可以跳过调用__init__但仍然实例化了类。有没有更好的方法来实现我的需要而不这样做?
  3. 注:

    我的问题有2个问题:

    1. 是否有必要测试各个成员的职能?
    2. (用于测试目的)是否有必要在不使用__init__初始化对象的情况下实例化类?
    3. 对于问题1,我进行了快速谷歌搜索并找到了相关的研究或讨论:

        

      我们最初通过设计测试来测试没有父母的基类   套件,单独测试每个成员函数,并测试   成员职能之间的互动。

      对于问题2,我不确定。但我认为有必要,如示例代码所示,func1和func2在__init__中调用。我觉得在没有使用__init__调用的类A对象上测试它们会更加舒适(因此之前没有调用func1和func2)。

      当然,可以用常规方法(testobj = A())实例化一个A类对象,然后对func1和func2进行单独测试。但这是好的:)?我在这里讨论的是测试这种情况的最佳方法是什么,优点和缺点是什么。

      另一方面,人们也可能会争辩说,从设计的角度来看,首先不应该在__init__中调用func1和func2。这是一个合理的设计选择吗?

3 个答案:

答案 0 :(得分:7)

在没有实例化类(包括运行__init__)的情况下测试类的方法通常没有用,甚至不可能。通常,您的类方法将引用类的属性(例如,self.a)。如果您不运行__init__,那么这些属性将不存在,因此您的方法将无法运行。 (如果你的方法不依赖于它们的实例的属性,那么为什么它们是方法而不仅仅是独立的函数?)在你的例子中,它看起来像func1func2是初始化的一部分过程,所以他们应该作为其中的一部分进行测试。

理论上,可以使用__new__“准实例化”该类,然后只添加您需要的成员,例如:

obj = A.__new__(args)
obj.a = "test value"
obj.func1()

然而,这可能不是一个很好的测试方法。首先,它会导致您复制可能已存在于初始化代码中的代码,这意味着您的测试更可能与实际代码不同步。另一方面,您可能必须以这种方式复制许多初始化调用,因为您必须手动重新执行从您的类调用的任何基类__init__方法所做的操作。

至于如何设计测试,您可以查看the unittest module和/或the nose module。这为您提供了如何设置测试的基础知识。实际放在测试中的内容显然取决于你的代码应该做什么。

编辑:问题1的答案是“肯定是的,但不一定是每一个”。你的问题2的答案是“可能不是”。即使在您提供的第一个链接中,也存在关于是否应该测试不属于该类的公共API的方法的争论。如果你的func1和func2纯粹是内部方法,只是初始化的一部分,那么可能没有必要在初始化时单独测试它们。

这是关于在__init__内调用func1和func2是否合适的最后一个问题。正如我在评论中反复说明的那样,这取决于这些功能的作用。如果func1和func2执行初始化的一部分(即,为实例做一些“设置”工作),那么从__init__调用它们是完全合理的。但是在这种情况下,它们应该作为初始化过程的一部分进行测试,而不需要单独测试它们。如果func1和func2 不是初始化的一部分,那么是的,你应该独立测试它们;但在那种情况下,为什么他们在__init__

构成实例化类的组成部分的方法应该作为测试类实例化的一部分进行测试。不应在__init__内调用不构成实例化类的组成部分的方法。

如果func1和func2“只是一个输入/输出逻辑”并且不需要访问该实例,那么它们根本不需要是该类的方法;它们可以只是独立的功能。如果你想将它们保存在类中,你可以将它们标记为staticmethods,然后直接在类上调用它们而不实例化它们。这是一个例子:

>>> class Foo(object):
...     def __init__(self, num):
...         self.numSquared = self.square(num)
...     
...     @staticmethod
...     def square(num):
...         return num**2
>>> Foo.square(2) # you can test the square "method" this way without instantiating Foo
4
>>> Foo(8).numSquared
64

可以想象你可能有一些需要极其复杂的初始化过程的怪物类。在这种情况下,您可能会发现有必要单独测试该过程的各个部分。然而,这样一个巨大的初始序列本身就是一个笨拙的设计的警告。

答案 1 :(得分:3)

如果您有选择,我会将初始化辅助函数声明为staticmethods,并从测试中调用它们。

如果你有不同的输入/输出值断言,你可以查看一些parametrizing examples with py.test

如果您的类实例化有点沉重,您可能需要查看dependency injection并像这样缓存实例:

# content of test_module.py

def pytest_funcarg__a(request):
    return request.cached_setup(lambda: A(...), scope="class")

class TestA:
    def test_basic(self, a):
        assert .... # check properties/non-init functions

这会在每个测试类中重复使用相同的“a”实例。其他可能的范围是“会话”,“功能”或“模块”。您还可以定义命令行选项以设置范围,以便快速开发使用更多缓存,对于Continous-Integration,您可以使用更多隔离的资源设置,而无需更改测试源代码。

就个人而言,在过去的12年里,我从细粒度的单元测试转向更多功能/集成类型的测试,因为它简化了重构,似乎更好地利用了我的整体时间。当失败发生时获得良好的支持和报告当然是至关重要的,例如掉线到PDB,简洁的追溯等等。对于一些错综复杂的算法,我仍然会编写非常细粒度的单元测试但是我通常将算法分离成非常独立的可测试的东西。

HTH,holger

答案 2 :(得分:0)

我同意之前的评论,通常最好通过减少实例化时完成的工作量来避免此问题,例如通过将func1等调用移到configure(self)方法中,该方法应在实例化后调用。

如果您有充分的理由在self.func1中保持对__init__等的调用,则pytest中有一种方法可能会有所帮助。

(1)将其放入模块中:

_called_from_test = False

(2)将以下内容放入conftest.py

import your_module

def pytest_configure(config):
    your_module._called_from_test = True

your_module命名。

(3)插入if语句以在运行测试时尽早结束__init__的执行,

   if _called_from_test:
      pass
   else:
      self.func1( ....)

然后您可以逐步完成各个函数调用,并在进行过程中对其进行测试。

_called_from_test设为__init__的可选参数也可以实现同样的目的。

pytest文档的Detect if running from within a pytest run部分提供了更多上下文。