例如:功能测试友好的DSL

备注

0.4版新增功能

这是一个DSL,用于编写使用昂贵的嵌套设备的测试——这通常意味着功能测试。它需要层插件(请参见 将测试夹具组织成层

它是什么样子的?

与一些测试DSL的Python不同,这只是普通的旧Python。

import unittest

from nose2.tools import such


class SomeLayer:
    @classmethod
    def setUp(cls):
        it.somelayer = True

    @classmethod
    def tearDown(cls):
        del it.somelayer


#
# Such tests start with a declaration about the system under test
# and will typically bind the test declaration to a variable with
# a name that makes nice sentences, like 'this' or 'it'.
#
with such.A("system with complex setup") as it:
    #
    # Each layer of tests can define setup and teardown methods.
    # setup and teardown methods defined here run around the entire
    # group of tests, not each individual test.
    #
    @it.has_setup
    def setup():
        it.things = [1]

    @it.has_teardown
    def teardown():
        it.things = []

    #
    # The 'should' decorator is used to mark tests.
    #
    @it.should("do something")
    def test():
        assert it.things
        #
        # Tests can use all of the normal unittest TestCase assert
        # methods by calling them on the test declaration.
        #
        it.assertEqual(len(it.things), 1)

    #
    # The 'having' context manager is used to introduce a new layer,
    # one that depends on the layer(s) above it. Tests in this
    # new layer inherit all of the fixtures of the layer above.
    #
    with it.having("an expensive fixture"):

        @it.has_setup  # noqa: F811
        def setup():  # noqa: F811
            it.things.append(2)

        #
        # Tests that take an argument will be passed the
        # unittest.TestCase instance that is generated to wrap
        # them. Tests can call any and all TestCase methods on this
        # instance.
        #
        @it.should("do more things")  # noqa: F811
        def test(case):  # noqa: F811
            case.assertEqual(it.things[-1], 2)

        #
        # Layers can be nested to any depth.
        #
        with it.having("another precondition"):

            @it.has_setup  # noqa: F811
            def setup():  # noqa: F811
                it.things.append(3)

            @it.has_teardown  # noqa: F811
            def teardown():  # noqa: F811
                it.things.pop()

            @it.should("do that not this")  # noqa: F811
            def test(case):  # noqa: F811
                it.things.append(4)
                #
                # Tests can add their own cleanup functions.
                #
                case.addCleanup(it.things.pop)
                case.assertEqual(it.things[-1], 4, it.things)

            @it.should("do this not that")  # noqa: F811
            def test(case):  # noqa: F811
                case.assertEqual(it.things[-1], 3, it.things[:])

        #
        # A layer may have any number of sub-layers.
        #
        with it.having("a different precondition"):
            #
            # A layer defined with ``having`` can make use of
            # layers defined elsewhere. An external layer
            # pulled in with ``it.uses`` becomes a parent
            # of the current layer (though it doesn't actually
            # get injected into the layer's MRO).
            #
            it.uses(SomeLayer)

            @it.has_setup  # noqa: F811
            def setup():  # noqa: F811
                it.things.append(99)

            @it.has_teardown  # noqa: F811
            def teardown():  # noqa: F811
                it.things.pop()

            #
            # Layers can define setup and teardown methods that wrap
            # each test case, as well, corresponding to TestCase.setUp
            # and TestCase.tearDown.
            #
            @it.has_test_setup
            def test_setup(case):
                it.is_funny = True
                case.is_funny = True

            @it.has_test_teardown
            def test_teardown(case):
                delattr(it, "is_funny")
                delattr(case, "is_funny")

            @it.should("do something else")  # noqa: F811
            def test(case):  # noqa: F811
                assert it.things[-1] == 99
                assert it.is_funny
                assert case.is_funny

            @it.should("have another test")  # noqa: F811
            def test(case):  # noqa: F811
                assert it.is_funny
                assert case.is_funny

            @it.should("have access to an external fixture")  # noqa: F811
            def test(case):  # noqa: F811
                assert it.somelayer

            with it.having("a case inside the external fixture"):

                @it.should("still have access to that fixture")  # noqa: F811
                def test(case):  # noqa: F811
                    assert it.somelayer


#
# To convert the layer definitions into test cases, you have to call
# `createTests` and pass in the module globals, so that the test cases
# and layer objects can be inserted into the module.
#
it.createTests(globals())


#
# Such tests and normal tests can coexist in the same modules.
#
class NormalTest(unittest.TestCase):
    def test(self):
        pass

它定义的测试是UnitTest测试,可以与Nose2一起使用,只使用层插件。您还可以选择激活报告插件 (nose2.plugins.layers.LayerReporter )要提供更具争议性的输出品牌:

test (test_such.NormalTest) ... ok
A system with complex setup
  should do something ... ok
  having an expensive fixture
    should do more things ... ok
    having another precondition
      should do that not this ... ok
      should do this not that ... ok
    having a different precondition
      should do something else ... ok
      should have another test ... ok

----------------------------------------------------------------------
Ran 7 tests in 0.002s

OK

它是如何工作的?

这样就使用了Python中最像匿名代码块的东西,允许您用有意义的名称和深度嵌套的设备构造测试。与允许使用块的语言中的DSL相比,它有点冗长——标记fixture方法和测试用例需要修饰的类似于块的修饰器。 某物 ,因此每个fixture和测试用例都必须有一个函数定义。您可以在这里使用相同的函数名,也可以为每个函数指定一个有意义的名称。

测试集从对整个测试系统的描述开始,并用 A 上下文管理器:

from nose2.tools import such

with such.A('system described here') as it:
    # ...

测试组由 having 上下文管理器:

with it.having('a description of a group'):
    # ...

在一个测试组(包括顶级组)内,用装饰器标记夹具:

@it.has_setup
def setup():
    # ...

@it.has_test_setup
def setup_each_test_case():
    # ...

测试也同样标有 should 装饰者:

@it.should('exhibit the behavior described here')
def test(case):
    # ...

测试用例可以选择采用一个参数。如果他们这样做,他们将通过 unittest.TestCase 为测试生成的实例。他们可以用这个 TestCase 执行断言方法的实例。如果测试函数不接受 case 论点:

@it.should("be able to use the scenario's assert methods")
def test():
    it.assertEqual(something, 'a value')

@it.should("optionally take an argument")
def test(case):
    case.assertEqual(case.attribute, 'some value')

最后,要实际生成测试,您需要 must 调用 createTests 在顶级方案实例上:

it.createTests(globals())

此调用生成 unittest.TestCase 所有测试的实例,以及保存测试组中定义的设备的层类。见 将测试夹具组织成层 有关测试层的更多信息。

运行试验

因为顺序在功能测试中通常很重要, 这样的DSL测试总是按照模块中定义的顺序执行。 . 父组在子组、组中的同级组和同级测试之前运行,并按照定义它们的顺序执行。

否则,将像其他任何测试一样收集和运行此类DSL中编写的测试,但有一个例外:它们的名称。这样一个测试用例的名称是它周围组的名称,加上测试的描述,前面加上 test ####: 在哪里 #### 是测试的 (0 -索引)其组中的位置。

要单独运行一个案例,您必须传递这个全名——通常您必须引用它。例如,运行案例 should do more things 上面定义的(假设层插件是由配置文件激活的,并且测试模块位于测试集合的正常路径中),您将运行nose2,如下所示:

nose2 "test_such.having an expensive fixture.test 0000: should do more things"

也就是说,对于生成的测试用例, 组说明类名测试用例描述测试用例名称 . 正如您可以看到的那样,如果在层报告器处于活动状态的情况下运行单个测试,那么当单独运行测试时,所有组设备都将按正确的顺序执行:

$ nose2 "test_such.having an expensive fixture.test 0000: should do more things"
A system with complex setup
  having an expensive fixture
    should do more things ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

参考文献

nose2.tools.such.A(description)[源代码]

测试方案上下文管理器。

返回A nose2.tools.such.Scenario 根据惯例,它必须 it

with such.A('test scenario') as it:
    # tests and fixtures
class nose2.tools.such.Scenario(description)[源代码]

测试场景。

测试场景定义了一组依赖于这些设备的设备和测试。

createTests(mod)[源代码]

为此方案生成测试用例。

警告

你必须打电话过来 globals() ,从方案生成测试。如果你不这样做, 不会创建任何测试 .

it.createTests(globals())
has_setup(func)[源代码]

添加 setup() 此组的方法。

这个 setup() 方法将在包含组中的任何测试之前运行一次。

一个组可以定义任意数量的 setup() 功能。它们将按照定义的顺序执行。

@it.has_setup
def setup():
    # ...
has_teardown(func)[源代码]

添加 teardown() 此组的方法。

这个 teardown() 方法将在包含组中的所有测试之后运行一次。

一个组可以定义任意数量的 teardown() 功能。它们将按照定义的顺序执行。

@it.has_teardown
def teardown():
    # ...
has_test_setup(func)[源代码]

添加测试用例 setup() 此组的方法。

这个 setup() 方法将在包含组中的每个测试之前运行。

一个组可以定义任意数量的测试用例 setup() 功能。它们将按照定义的顺序执行。

试验 setup() 函数可以选择接受一个参数。如果他们这样做,他们将通过 unittest.TestCase 为测试生成的实例。

@it.has_test_setup
def setup(case):
    # ...
has_test_teardown(func)[源代码]

添加测试用例 teardown() 此组的方法。

这个 teardown() 方法将在包含组中的每个测试之前运行。

一个组可以定义任意数量的测试用例 teardown() 功能。它们将按照定义的顺序执行。

试验 teardown() 函数可以选择接受一个参数。如果他们这样做,他们将通过 unittest.TestCase 为测试生成的实例。

@it.has_test_teardown
def teardown(case):
    # ...
having(description)[源代码]

在当前组下定义新组。

块中定义的装置和测试将属于新组。

with it.having('a description of this group'):
    # ...
should(desc)[源代码]

定义测试用例。

用这个修饰符标记的每个函数都成为当前组中的测试用例。

decorator接受一个可选参数,测试用例的描述:它是什么 应该 做。如果不提供此参数,则将使用修饰函数的docstring作为测试用例描述。

测试函数可以选择接受一个参数。如果他们这样做,他们将通过 unittest.TestCase 为测试生成的实例。他们可以使用这个测试用例实例来执行断言方法等。

@it.should('do this')
def dothis(case):
    # ....

@it.should
def dothat():
    "do that also"
    # ....