简体   繁体   English

使用 python 单元测试的抽象测试用例

[英]abstract test case using python unittest

Is it possible to create an abstract TestCase , that will have some test_* methods, but this TestCase won't be called and those methods will only be used in subclasses?是否可以创建一个抽象的TestCase ,它会有一些 test_* 方法,但是不会调用这个TestCase并且这些方法只会在子类中使用? I think I am going to have one abstract TestCase in my test suite and it will be subclassed for a few different implementation of a single interface.我想我将在我的测试套件中拥有一个抽象的TestCase ,它将为单个接口的几个不同实现进行子类化。 This is why all test methods are the some, only one, internal method changes.这就是为什么所有的测试方法都是一些,只有一种,内部方法变化。 How can I do it in elegant way?我怎样才能以优雅的方式做到这一点?

I didn't quite understand what do you plan to do -- the rule of thumb is "not to be smart with tests" - just have them there, plain written.我不太明白你打算做什么——经验法则是“不要对测试很聪明”——只要把它们放在那里,写得很简单。

But to achieve what you want, if you inherit from unittest.TestCase, whenever you call unittest.main() your "abstract" class will be executed - I think this is the situation you want to avoid.但是为了实现你想要的,如果你从 unittest.TestCase 继承,每当你调用 unittest.main() 你的“抽象”类将被执行 - 我认为这是你想要避免的情况。

Just do this: Create your "abstract" class inheriting from "object", not from TestCase.只需这样做:创建从“对象”而不是从 TestCase 继承的“抽象”类。 And for the actual "concrete" implementations, just use multiple inheritance: inherit from both unittest.TestCase and from your abstract class.对于实际的“具体”实现,只需使用多重继承:从 unittest.TestCase 和抽象类继承。

import unittest

class Abstract(object):
    def test_a(self):
        print "Running for class", self.__class__

class Test(Abstract, unittest.TestCase):
    pass

unittest.main()

update : reversed the inheritance order - Abstract first so that its defintions are not overriden by TestCase defaults, as well pointed in the comments bellow.更新:颠倒继承顺序 - 首先Abstract以便其定义不会被TestCase默认值覆盖,并在下面的评论中指出。

There's a very simple way that everyone has missed so far.到目前为止,每个人都错过了一个非常简单的方法。 And unlike several of the answers, it works with all test drivers, rather than failing the minute you switch between them.与几个答案不同,它适用于所有测试驱动程序,而不是在您在它们之间切换时失败。

Simply use inheritence as usual, then add:像往常一样简单地使用继承,然后添加:

del AbstractTestCase

at the end of the module.在模块的最后。

Multiple inheritance isn't a great option here, chiefly for the two following reasons:多重继承在这里不是一个很好的选择,主要有以下两个原因:

  1. None of the methods in TestCase use super() so you'd have to list your class first for methods like setUp() and tearDown() to work. TestCase中的任何方法都没有使用super()因此您必须首先列出您的类,这样setUp()tearDown()才能工作。
  2. pylint will warn that the base class uses self.assertEquals() etc which aren't defined on self at that point. pylint 会警告基类使用self.assertEquals()等,这些都没有在self上定义。

Here's the kludge I came up with: turn run() into a no-op for the base class only.这是我想出的杂牌:将run()变成仅用于基类的无操作。

class TestBase( unittest.TestCase ):

  def __init__( self, *args, **kwargs ):
    super( TestBase, self ).__init__( *args, **kwargs )
    self.helper = None
    # Kludge alert: We want this class to carry test cases without being run
    # by the unit test framework, so the `run' method is overridden to do
    # nothing.  But in order for sub-classes to be able to do something when
    # run is invoked, the constructor will rebind `run' from TestCase.
    if self.__class__ != TestBase:
      # Rebind `run' from the parent class.
      self.run = unittest.TestCase.run.__get__( self, self.__class__ )                          
    else:
      self.run = lambda self, *args, **kwargs: None

  def newHelper( self ):
    raise NotImplementedError()

  def setUp( self ):
    print "shared for all subclasses"
    self.helper = self.newHelper()

  def testFoo( self ):
    print "shared for all subclasses"
    # test something with self.helper

class Test1( TestBase ):
  def newHelper( self ):
    return HelperObject1()

class Test2( TestBase ):
  def newHelper( self ):
    return HelperObject2()

If you really want to use inheritance instead of mixins, a simple solution is to nest the abstract test in another class.如果你真的想使用继承而不是mixin,一个简单的解决方案是将抽象测试嵌套在另一个类中。

It avoids issues with test runner discovery and you can still import the abstract test from another module.它避免了测试运行程序发现的问题,您仍然可以从另一个模块导入抽象测试。

import unittest

class AbstractTests(object):
    class AbstractTest(unittest.TestCase)
        def test_a(self):
            print "Running for class", self.__class__

class Test(AbstractTests.AbstractTest):
    pass

Just to put in my two-cents, although it likely goes against some convention, you could define your abstract test case as a protected member to prevent its execution.只是投入我的两分钱,尽管它可能违反某些约定,但您可以将抽象测试用例定义为受保护的成员以防止其执行。 I've implemented the following in Django and works as required.我已经在 Django 中实现了以下内容并按要求工作。 See example below.请参阅下面的示例。

from django.test import TestCase


class _AbstractTestCase(TestCase):

    """
    Abstract test case - should not be instantiated by the test runner.
    """

    def test_1(self):
        raise NotImplementedError()

    def test_2(self):
        raise NotImplementedError()


class TestCase1(_AbstractTestCase):

    """
    This test case will pass and fail.
    """

    def test_1(self):
        self.assertEqual(1 + 1, 2)


class TestCase2(_AbstractTestCase):

    """
    This test case will pass successfully.
    """

    def test_1(self):
        self.assertEqual(2 + 2, 4)

    def test_2(self):
        self.assertEqual(12 * 12, 144)

Raise unittest.SkipTest in setUpClass()setUpClass()提高unittest.SkipTest

Just another approach is to raise a unittest.SkipTest in setUpClass() of the base class and override setUpClass() in child classes:只是另一种方法是提高unittest.SkipTestsetUpClass()的基类,覆盖的setUpClass()在子类:

class BaseTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        "Child classes must override this method and define cls.x and cls.y"
        raise unittest.SkipTest

    def test_x(self):
        self.assertEqual(self.x * 3, self.x)

    def test_y(self):
        self.assertEqual(self.y * 3, self.y + self.y + self.y)

    def test_z(self):
        self.assertEqual(self.x + self.y, self.y)


class IntegerTestCase(BaseTestCase):
    @classmethod
    def setUpClass(cls):
        cls.x = 0
        cls.y = 2


class StringTestCase(BaseTestCase):
    @classmethod
    def setUpClass(cls):
        cls.x = ''
        cls.y = 'zuzuka'

If you need to use custom TestCase that defines its own setUpClass() and you need to call super().setUpClass() , you can define you own method to "set up data" and raise SkipTest only inside that method:如果您需要使用定义其自己的setUpClass()自定义 TestCase 并且您需要调用super().setUpClass() ,您可以定义您自己的方法来“设置数据”并仅在该方法内引发 SkipTest:

class BaseTestCase(ThidPartyTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()  # if ThirdPartyTestCase has own setUpClass()
        cls.setUpTestCaseData()

    @classmethod
    def setUpTestCaseData(cls):
        "Override and set up cls.x and cls.y here"
        raise unittest.SkipTest

    ...  # tests


class IntegerTestCase(BaseTestCase):
    @classmethod
    def setUpTestCaseData(cls):
        cls.x = 0
        cls.y = 2

If you follow the convention of explicitly listing all test classes in run_unittest (see eg the Python test suite for many uses of that convention), then it will be straight-forward to not list a specific class.如果您遵循在 run_unittest 中显式列出所有测试类的约定(参见 Python 测试套件以了解该约定的许多用途),那么列出特定类将是直接的。

If you want to continue using unittest.main, and if you can allow using unittest2 (eg from Python 2.7), you can use its load_tests protocol to specify which classes contain test cases).如果您想继续使用 unittest.main,并且您可以允许使用 unittest2(例如来自 Python 2.7),您可以使用它的load_tests协议来指定哪些类包含测试用例)。 In earlier versions, you will have to subclass TestLoader, and override loadTestsFromModule .在早期版本中,您必须继承 TestLoader 并覆盖loadTestsFromModule

Python unittest library has load_tests protocol , which can be used to achieve exactly what do you want: Python unittest 库有load_tests 协议,它可以用来实现你想要的:

# Add this function to module with AbstractTestCase class
def load_tests(loader, tests, _):
    result = []
    for test_case in tests:
        if type(test_case._tests[0]) is AbstractTestCase:
            continue
        result.append(test_case)
    return loader.suiteClass(result)

Another reason for wanting to do what the OP is doing is to create a highly-parameterized base class which implements much of a set of core tests which need to be reproduced in several environments/scenarios.想要做 OP 正在做的事情的另一个原因是创建一个高度参数化的基类,它实现了许多需要在多个环境/场景中重现的核心测试。 What I'm describing is essentially creating a parameterized fixture, a la pytest, using unittest.我所描述的本质上是使用 unittest 创建一个参数化的装置,一个 la pytest。

Assuming you (like me) decide to run away as fast as you can from any multiple-inheritance-based solutions, one might have the following problem with using load_tests() to filter out your base class from the loaded suite:假设您(像我一样)决定尽快逃离任何基于多继承的解决方案,那么使用 load_tests() 从加载的套件中过滤掉您的基类时可能会遇到以下问题:

In the standard TestLoader, load_tests is called after the auto-loading-from-class is done.在标准的 TestLoader 中,load_tests 在类自动加载完成后被调用。 Because: * this auto-loading-from-class will attempt to construct instances from your base class with the standard signature init (self, name), and * you may want this base class to have a very different ctor signature, or * you may want to skip construction-then-removal of your base class instances for some other reason因为: * 这个从类自动加载的将尝试使用标准签名init (self, name) 从你的基类构造实例,并且 * 你可能希望这个基类有一个非常不同的 ctor 签名,或者 * 你由于某些其他原因,您可能希望跳过基类实例的构建然后删除

.. you may want to completely prevent this auto-loading of test instances from base classes. .. 您可能希望完全阻止从基类自动加载测试实例。

EDIT: Vadim's solution in this other thread is a more elegant, concise, and independent way to do this.编辑: Vadim 在另一个线程中的解决方案是一种更优雅、简洁和独立的方法。 I have implemented the "nested class trick" and confirmed it works beautifully for the purpose of preventing TestLoader from "finding" your TestCase bases.我已经实现了“嵌套类技巧”,并确认它可以很好地工作,以防止 TestLoader “找到”您的 TestCase 基础。

I originally had done this by modifying TestLoader.loadTestsFromModule to simply skip any TestCase classes which serve as base classes for any other TestCase classes in the module:我最初是通过修改 TestLoader.loadTestsFromModule 来简单地跳过作为模块中任何其他 TestCase 类的基类的任何 TestCase 类来完成的:

for name in dir(module):
    obj = getattr(module, name)
    # skip TestCase classes:
    # 1. without any test methods defined
    # 2. that are base classes
    #    (we don't allow instantiating TestCase base classes, which allows test designers
    #     to implement actual test methods in highly-parametrized base classes.)
    if isinstance(obj, type) and issubclass(obj, unittest.TestCase) and \
            self.getTestCaseNames(obj) and not isbase(obj, module):
        loaded_suite = self.loadTestsFromTestCase(obj)
        # ignore empty suites
        if loaded_suite.countTestCases():
            tests.append(loaded_suite)

where:其中:

def isbase(cls, module):
    '''Returns True if cls is base class to any classes in module, else False.'''
    for name in dir(module):
        obj = getattr(module, name)
        if obj is not cls and isinstance(obj, type) and issubclass(obj, cls):
            return True
    return False

The parametrization I spoke of above is implemented by having each child class define it's fixture details (the parameters) and pass them to the base class TestCase ctor so that all of its common impl methods (the "fixturey" ones setUp*/tearDown*/cleanup* and the test methods themselves) have all the info that defines the now very specific fixture that that child TestCase class is to operate on.我上面提到的参数化是通过让每个子类定义它的夹具细节(参数)并将它们传递给基类 TestCase ctor 来实现的,这样它的所有常见的 impl 方法(“fixturey”的 setUp*/tearDown*/ cleanup*测试方法本身)具有定义子 TestCase 类要操作的现在非常具体的夹具的所有信息。

For me, this was a temporary solution for quickly implementing some parametrized fixtures in unittest, since I plan to move my team's tests to pytest asap.对我来说,这是在 unittest 中快速实现一些参数化装置的临时解决方案,因为我计划尽快将我团队的测试转移到 pytest。

The unittest module provides several options for skipping tests . unittest模块提供了几个跳过测试的选项。

My preferred solution is to override the setUpClass method in the "abstract" base class to raise a unittest.SkipTest exception if needed:我的首选解决方案是覆盖“抽象”基类中的setUpClass方法以在需要时引发unittest.SkipTest异常:

class BaseTestCase(unittest.TestCase):
  @classmethod
  def setUpClass(cls):
    if cls is BaseTestCase:
      raise unittest.SkipTest("%s is an abstract base class" % cls.__name__)
    else:
      super(BaseTestCase, cls).setUpClass()

I have done it following way, maybe it can inspire you:我已经按照以下方式完成了,也许它可以激励你:

class AbstractTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def _test_1(self):
        # your test case here

class ConcreteTest(AbstractTest)

    def test_1(self):
        self._test_1()

Although it's not the most convenient solution, it lets you escape from multi inheritance.虽然这不是最方便的解决方案,但它可以让您摆脱多继承。 Also, the solution suggested by Dan Ward didn't work with Django testing in PyCharm.此外,Dan Ward 建议的解决方案不适用于 PyCharm 中的 Django 测试。

Here's a relatively simple approach that allows your common tests to inherit from TestCase (so type checking and IDE tooling stays happy), that uses only documented unittest features, and that avoids the "skip" test status:这是一个相对简单的方法,它允许您的常用测试从 TestCase 继承(因此类型检查和 IDE 工具保持愉快),它仅使用记录的单元测试功能,并避免“跳过”测试状态:

import unittest

class CommonTestCases(unittest.TestCase):
    def __init__(self, methodName='runTest'):
        if self.__class__ is CommonTestCases:
            # don't run these tests on the abstract base implementation
            methodName = 'runNoTestsInBaseClass'
        super().__init__(methodName)

    def runNoTestsInBaseClass(self):
        print('not running tests in abstract base class')
        pass

    def test_common(self):
        # This will run *only* in subclasses. Presumably, this would 
        # be a test you need to repeat in several different contexts.
        self.assertEqual(2 + 2, 4)


class SomeTests(CommonTestCases):
    # inherited test_common *will* be run here

    def test_something(self):
        self.assertTrue(True)


# Also plays nicely with MRO, if needed:
class SomeOtherTests(CommonTestCases, django.test.SimpleTestCase):
    # inherited test_common *will* be run here

    def test_something_else(self):
        self.client.get('/')  # ...

How it works: per the unittest.TestCase documentation , "Each instance of TestCase will run a single base method: the method named methodName."工作原理:根据unittest.TestCase文档,“TestCase 的每个实例都将运行一个基本方法:名为 methodName 的方法。” The default "runTests" runs all the test* methods on the class—that's how TestCase instances normally work.默认的“runTests”运行类上的所有 test* 方法——这就是 TestCase 实例的正常工作方式。 But when running in the abstract base class itself, you can simply override that behavior with a method that does nothing.但是当在抽象基类本身中运行时,您可以简单地使用什么都不做的方法覆盖该行为。

A side effect is your test count will increase by one: the runNoTestsInBaseClass "test" gets counted as a successful test when it's run on CommonTestCases.一个副作用是您的测试计数将增加 1:runNoTestsInBaseClass “测试”在 CommonTestCases 上运行时被计为成功测试。

If you set __test__ = False in the base test class it should disable its tests.如果您在基础测试 class 中设置__test__ = False ,它应该禁用其测试。 Quoting from this link :从此链接引用:

 class MessageTestBase(unittest.TestCase): __test__ = False def setUp(self): self.status = 'running' def tearDown(self): self.status = 'not running' def test_common_test(self): self.assertEqual(self.status, 'running') class TestSlack(MessageTestMixin): __test__ = True

Notice the differences.注意差异。 Our mixin becomes a base class that inherits from TestCase.我们的 mixin 成为继承自 TestCase 的基础 class。 we include __test__ = False in the base class to prevent the test runner from executing tests in this class.我们在基础 class 中包含__test__ = False ,以防止测试运行程序在此 class 中执行测试。 Then the child class only inherits from MessageTestBase and includes __test__ = True to instruct the test runner to run our tests.然后子 class 仅继承自 MessageTestBase 并包含__test__ = True以指示测试运行器运行我们的测试。

More details here: How does __test__ = False magic attribute work for test discovery此处有更多详细信息: __test__ = False 魔术属性如何用于测试发现

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

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