跳转到主要内容

测试助手。

项目描述

https://api.travis-ci.org/Fluxx/exam.png?branch=master

Exam

https://dl.dropbox.com/u/3663715/exam.jpeg

Exam是Python编写更好测试的工具包。它旨在删除许多经常编写的样板测试代码,同时仍然遵循Python约定并遵守单元测试接口。

安装

简单的pip install exam即可。

原理

除了明显的“代码是否工作?”之外,编写测试还有许多额外的目标和好处。

  1. 如果编写得语义清晰,阅读测试可以帮助其他开发者了解代码应该如何工作。

  2. 如果运行速度快,测试可以在开发过程中提供反馈,告诉您更改是否工作或是否产生副作用。

  3. 如果编写正确容易,开发者将编写更多测试,并且测试质量会更高。

不幸的是,编写Python单元测试的常见模式往往不提供这些优势。通常会导致效率低下且不必要的晦涩测试代码。此外,对mock库的常见使用往往会导致重复的样板代码或在测试运行期间效率低下。

exam旨在通过提供一组有用的功能,使编写快速、正确且有用的测试尽可能无痛苦,从而改善Python测试编写的状态。

使用方法

Exam提供了一系列有用的模块。

exam.decorators

考试提供了一些有用的装饰器,可以让你的测试更容易编写和理解。要使用@before@after@around@patcher装饰器,你必须将exam.cases.Exam类混入你的测试用例中。它实现了必要的setUp()tearDown()方法,使装饰器能够正常工作。

请注意,@fixture装饰器无需在Exam类内部定义即可使用。然而,将Exam混入测试用例是一个好的实践。

exam.decorators中的所有装饰器以及Exam测试用例都可以从主exam包中导入。例如:

from exam import Exam
from exam import fixture, before, after, around, patcher
exam.decorators.fixture

@fixture装饰器将方法转换为属性(类似于@property装饰器,但还会缓存返回值)。这让你可以在测试中引用该属性,例如self.grounds,它将始终引用完全相同的实例。

from exam.decorators import fixture
from exam.cases import Exam

class MyTest(Exam, TestCase):

    @fixture
    def user(self):
        return User(name='jeff')

    def test_user_name_is_jeff(self):
        assert self.user.name == 'jeff'

如你所见,self.user用于引用上面定义的user属性。

如果你的固定方法只是构造一个新实例或调用一个类方法,exam提供了简写的内联fixture语法来构造固定对象。只需将类变量设置为fixture(type_or_class_method),exam将自动调用你的类型或类方法。

from exam.decorators import fixture
from exam.cases import Exam

class MyTest(Exam, TestCase):

    user = fixture(User, name='jeff')

    def test_user_name_is_jeff(self):
        assert self.user.name == 'jeff'

传递给fixture(type_or_class_method)的任何*或**kwargs都将传递给调用时的type_or_class_method

exam.decorators.before

@before装饰器将方法添加到类setUp()例程运行的列表中。

from exam.decorators import before
from exam.cases import Exam

class MyTest(Exam, TestCase):

    @before
    def reset_database(self):
        mydb.reset()

@before装饰器也可以通过子类工作 - 这意味着,如果父类中有一个@before钩子,而你子类化了它并在其中定义了第二个@before钩子,两个@before钩子都会被调用。Exam首先运行父类的@before钩子,然后运行子类的。此外,如果你在子类中覆盖了一个@before钩子,覆盖的方法将在运行子类其他@before钩子时运行。

例如,当钩子定义如下

from exam.decorators import before
from exam.cases import Exam

class MyTest(Exam, TestCase):

    @before
    def reset_database(self):
        print 'parent reset_db'

    @before
    def parent_hook(self):
        print 'parent hook'


class RedisTest(MyTest):

    @before
    def reset_database(self):
        print 'child reset_db'

    @before
    def child_hook(self):
        print 'child hook'

当Exam运行这些钩子时,输出将会是

"prent hook"
"child reset_db"
"child hook"

如你所见,尽管父类定义了一个reset_database,但由于子类覆盖了它,所以运行的是子类的版本,并且同时运行了子类其他@before钩子。

@before钩子也可以用你的测试用例中的其他函数构建,用于装饰实际的测试方法。当使用此策略时,Exam将在运行特定的测试方法之前运行由@before构建的函数。

from exam.decorators import before, fixture
from exam.cases import Exam

from myapp import User

class MyTest(Exam, TestCase):

    user = fixture(User)

    @before
    def create_user(self):
        self.user.create()

    def confirm_user(self):
        self.user.confirm()

    @before(confirm_user)
    def test_confirmed_users_have_no_token(self):
        self.assertFalse(self.user.token)

    def test_user_display_name_exists(self):
        self.assertTrue(self.user.display_name)

在上面的示例中,confirm_user方法在test_confirmed_users_have_no_token方法之前立即运行,但不会运行test_user_display_name_exists方法。全局装饰的@beforecreate_user方法仍然在每次测试方法之前运行。

@before也可以用多个函数构建,在运行测试方法之前调用

class MyTest(Exam, TestCase):

    @before(func1, func2)
    def test_does_things(self):
        does_things()

在上面的示例中,func1func2在运行test_does_things之前按顺序调用。

exam.decorators.after

@before@after的补充,@after方法将此方法添加到类tearDown()例程运行的方法列表中。与@before一样,@after在运行子类中定义的方法之前,会运行父类的@after钩子。

from exam.decorators import after
from exam.cases import Exam

class MyTest(Exam, TestCase):

    @after
    def remove_temp_files(self):
        myapp.remove_temp_files()
exam.decorators.around

使用@around装饰的方法作为每个测试方法的上下文管理器。在您的around方法中,您负责在您希望测试用例运行的地方调用yield

from exam.decorators import around
from exam.cases import Exam

class MyTest(Exam, TestCase):

    @around
    def run_in_transaction(self):
        db.begin_transaction()
        yield  # Run the test
        db.rollback_transaction()

@around同样遵循与@before@after相同的父/子排序规则,因此父@arounds将在(直到yield语句)运行,然后是子@around。然而,测试方法完成后,将运行子@around的其余部分,然后是父类。这样做是为了保持上下文管理器的嵌套的正常行为。

exam.decorators.patcher

@patcher装饰器是以下样板代码的简写

from mock import patch

 def setUp(self):
     self.stats_patcher = patch('mylib.stats', new=dummy_stats)
     self.stats = self.stats_patcher.start()

 def tearDown(self):
     self.stats_patcher.stop()

通常,手动控制补丁的启动/停止是为了提供测试用例属性(在此处为self.stats)供您要补丁的模拟对象使用。如果您希望模拟对象对大多数测试具有默认行为,但对其中的某些测试进行轻微更改(例如,大多数时候吸收所有调用,但对于某些测试则引发异常),这将很有用。

使用@patcher装饰器,上述代码可以简单地写成以下形式

from exam.decorators import patcher
from exam.cases import Exam

class MyTest(Exam, TestCase):

   @patcher('mylib.stats')
   def stats(self):
       return dummy_stats

Exam负责适当地启动和停止补丁器,以及使用带有装饰方法返回值的补丁对象构造补丁对象。

如果您对补丁的默认构造的模拟对象(MagicMock)感到满意,那么补丁器可以简单地作为类体内部的函数内联使用。此方法在需要时仍然启动和停止补丁器,并返回构造的MagicMock对象,您可以将它设置为类属性。Exam将自动将MagicMock对象添加到测试用例作为实例属性。

from exam.decorators import patcher
from exam.cases import Exam

class MyTest(Exam, TestCase):

    logger = patcher('coffee.logger')
exam.decorators.patcher.object

patcher.object装饰器提供了与patcher装饰器相同的功能,但它用于补丁对象的属性(类似于模拟的mock.patch.object)。例如,以下是使用补丁器补丁Userobjects属性的示例

from exam.decorators import patcher
from exam.cases import Exam

from myapp import User

class MyTest(Exam, TestCase):

    manager = patcher.object(User, 'objects')

与原生的patcher一样,在您的测试用例中,self.manager将是用补丁替换的模拟对象。

exam.helpers

helpers模块提供了一组辅助方法,用于常见测试模式

exam.helpers.track

track辅助程序旨在帮助跟踪独立模拟对象的调用顺序。track使用kwargs调用,其中键是模拟名称(一个字符串),值是您要跟踪的模拟对象。track返回一个新构造的MagicMock对象,每个模拟对象都附加在以模拟名称命名的属性上。

例如,下面的track()创建一个新的模拟,其中tracker.cool`cool_mocktracker.heatheat_mock

from exam.helpers import track

@mock.patch('coffee.roast.heat')
@mock.patch('coffee.roast.cool')
def test_roasting_heats_then_cools_beans(self, cool_mock, heat_mock):
    tracker = track(heat=heat_mock, cool=cool_mock)
    roast.perform()
    tracker.assert_has_calls([mock.call.heat(), mock.call.cool()])
exam.helpers.rm_f

这是一个简单的辅助程序,仅删除路径上的所有文件夹和文件

from exam.helpers import rm_f

rm_f('/folder/i/do/not/care/about')
exam.helpers.mock_import

移除大多数用于模拟导入的样板代码,这些代码通常涉及从sys.modules创建一个patch.dict。相反,可以使用patch_import辅助工具作为装饰器或上下文管理器,在导入某些模块时使用。

from exam.helpers import mock_import

with mock_import('os.path') as my_os_path:
    import os.path as imported_os_path
    assert my_os_path is imported_os_path

mock_import也可以用作装饰器,它将模拟值传递给测试方法(类似于正常的@patch)装饰器。

from exam.helpers import mock_import

@mock_import('os.path')
def test_method(self):
    import os.path as imported_os_path
    assert my_os_path is imported_os_path
exam.helpers.effect

这是一个本身可调用的辅助类,其返回值通过构造函数传入的元组进行配置。用于为Mock对象构建side_effect调用。如果调用时使用未配置的参数,则抛出TypeError。

>>> from exam.objects import call, effect
>>> side_effect = effect((call(1), 'with 1'), (call(2), 'with 2'))
>>> side_effect(1)
'with 1'
>>> side_effect(2)
'with 2'

通过配置元组中传入到effect构造函数的call`对象的等价性(==)来检查调用参数的等价性,这是配置元组的第0个项。默认情况下,call对象只是mock.call对象。

如果您想自定义此行为,可以子类化effect并重新定义自己的call_class类变量。例如:

class myeffect(effect):
    call_class = my_call_class

exam.mock

Exam有一个正常的mock.Mock对象的子类,它为您的模拟对象添加了一些更有用的方法。将其用于普通Mock对象的位置。

from exam.mock import Mock

mock_user = Mock(spec=User)

该子类有以下额外的方法

  • assert_called() - 断言模拟至少被调用了一次。

  • assert_not_called() - 断言模拟从未被调用。

  • assert_not_called_with(*args, **kwargs) - 断言模拟最近未被指定的*args**kwargs调用。

  • assert_not_called_once_with(*args, **kwargs) - 断言模拟只被指定的*args**kwargs调用过一次。

  • assert_not_any_call(*args, **kwargs) - 断言模拟从未被指定的*args**kwargs调用。

exam.fixtures

有助于在测试中使用的一些固定装置。

  • exam.fixtures.two_px_square_image - 2px正方形图像的数据作为字符串。

  • exam.fixtures.one_px_spacer - 1px正方形空白图像的数据作为字符串。

exam.objects

用于测试的有用对象。

exam.objects.noop - 总是返回None的调用对象,无论如何调用。

exam.asserts

asserts模块包含一个AssertsMixin类,该类被混合到主要的Exam测试用例混合器中。它包含超出Python的unittest的额外断言。

assertChanges

当您想断言代码的一部分改变了值时使用。例如,假设您有一个改变士兵军衔的函数。

为了正确测试这一点,您应该将士兵的军衔保存到临时变量中,然后运行该函数以更改军衔,最后断言军衔是新的预期值,以及是旧值。

test_changes_rank(self):
    old_rank = self.soldier.rank
    promote(self.soldier, 'general')
    self.assertEqual(self.soldier.rank, 'general')
    self.assertNotEqual(self.soldier.rank, old_rank)

检查旧军衔不等于新军衔很重要。如果,由于某种原因存在错误或某些地方,self.soldier被创建为general军衔,但promote不起作用,此测试仍会通过!

为了解决这个问题,您可以使用Exam的assertChanges

def test_changes_rank(self):
    with self.assertChanges(getattr, self.soldier, 'rank', after='general'):
        promote(self.soldier, 'general')

此断言正在做几件事情。

  1. 它断言在上下文运行后军衔是预期的general

  2. 它断言上下文会改变self.soldier.rank的值。

  3. 它实际上并不关心self.soldier.rank的旧值是什么,只要在上下文运行时发生了改变。

assertChanges的定义是

def assertChanges(thing, *args, **kwargs)
  1. 你给它一个“东西”,这个“东西”可以是一个可调用对象。

  2. assertChanges随后会使用任何传递的*args**kwargs来调用你的“东西”,并将捕获的值作为“之前”的值。

  3. 运行上下文,然后再次捕获可调用对象作为“之后”的值。

  4. 如果之前和之后没有不同,将引发一个AssertionError

  5. 此外,如果传递了特殊的kwarg beforeafter,将提取并保存这些值。在这种情况下,如果提供的“之前”和/或“之后”值与提取的值不匹配,也可以引发一个AssertionError

assertDoesNotChange

assertDoesNotChangeassertChanges类似,断言上下文内的代码不会改变可调用的值

def test_does_not_change_rank(self):
    with self.assertDoesNotChange(getattr, self.soldier, 'rank'):
        self.soldier.march()

assertChanges不同,assertDoesNotChange不接受beforeafter kwarg。它只是断言当上下文运行时,可调用的值没有改变。

许可证

本考试是MIT许可的。请参阅LICENSE文件以获取详细信息。

支持者:

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面