单元测试


本文档主要针对想帮助开发Evennia的人。

单元测试的目的是对程序组件做单独的测试,以确保每个部分在加入整体之前都是能正常工作的。广泛的测试有助于避免代码更新带来的意外副作用,还可以减少不良代码(在这篇维基百科文章中可以看到关于单元测试的详细信息)。

典型的单元测试会用指定输入调用Evennia的某些部分,然后观察结果是否与预期相符。与使用大量的独立测试程序不同,Evennia使用测试中心做测试。这是一段程序,集中了Evennia源码中所有的可用测试(称为测试套件),它可以一次运行所有的测试。所有的错误和回溯都会记录下来。


运行测试套件

要运行Evenniad的测试套件,可以前往 game/ 文件夹,并执行命令
python manage.py test
在测试时会使用临时的数据库。如果一切顺利,你会看到花了所少时间运行了多少测试;如果出了问题,你会得到错误信息。


编写新测试

Evennia的测试套件使用了Django的单元测试系统,而它又依赖于Python的 unittest 模块。

Evennia的主要测试包是从 src/server/tests.py 创建的。如果你看过该模块的导入部分就会发现大多数测试都在 src/tests/ 中(也有些不在其中,如 commands/default/tests.py)。在 src/tests/ 中有许多测试evennia子系统的文件,目前大部分是空的模板,今后会逐步加入更多测试。

unittest.TestCase 类可以以多种方式测试某方面的功能或某个组件。每个测试用例都带有一个或多个测试方法,其中定义了实际执行的测试代码。你可以给测试方法起任意的名字,只要它们是以“test_”开头的就行。你的 TestCase 类还可以有一个 SetUp() 方法,它会在每次测试前执行,可以用来创建、保存测试方法所需要的任何东西。

要检验结果,你可以用 TestCase 类中的一些特殊方法,它们大多以“assert”开头,比如 assertEqual 和 assertTrue。

TestCase类的例子:
    import unittest

    # 要测试的函数
    from mypath import myfunc

    TestObj(unittest.TestCase):
        "测试函数myfunc。"

        def test_return_value(self):
            "测试方法,确认返回值符合预期。"
            expected_return = "我很好。"
            actual_return = myfunc()
            # 检验
            self.assertEqual(expected_return, actual_return)
        def test_alternative_call(self):
            "测试方法,使用关键字参数调用。"
            expected_return = "我不好。"
            actual_return = myfunc(bad=True)
            # 检验
            self.assertEqual(expected_return, actual_return)
上面的例子非常简单,但你应该能明白它的含义。你可以查阅 src/tests/test_utils_utils.py 中的测试例子。你还可以阅读 unittest 模块的文档


测试游戏内的命令

游戏中的命令是特例,默认命令的测试代码在 src/commands/default/tests.py 中。该测试套件是作为对象测试套件的一部分执行的(因为它不属于Django的“应用程序”)。它还为执行命令提供了一些便捷的功能(特别是创建一个“假”玩家会话用以模仿实际的命令调用)。它还创建了一些可用的测试角色和物体,比如 char1 是一个“已登录”的玩家对象,它扮演命令的调用者。

每个命令都应该有自己的 TestCase 类,这个类可以从同一模块中的 CommandTest 类继承,这样就可以使用前面提到的命令专用工具了。
    class TestSet(CommandTest):
    "通过简单调用测试@set命令"
    def test_call(self):
        self.execute_command("@set self/testval = 我的测试值")
        # 按照@set的功能,我们的测试角色(char1)现在
        # 应该有一个新属性“testval”并具有值“我的测试值”。
        self.assertEqual("我的测试值", self.char1.db.testval)


关于添加新测试的注意事项

在开发Evennia的过程中,广泛地使用测试套件对避免代码退化非常重要。目前,测试套件仅覆盖了Evennia代码库的一小部分。编写新的测试并不难,把时间用在这上面是非常值得的。每个人都可以在添加新测试方面做出贡献,这只需要有限的Python知识就行了。

如果你想提供帮助,可以从扩充 src/tests/ 中的空模板开始。以下是测试类型的纲要,按所需的知识排序:
  • src/tests/test_utils_utils.py —— 编写这里的测试可能是最简单的,因为工具函数都是非常独立的,具有明确定义的输入和输出。
  • src/tests/test_utils_* —— 编写单元测试比较容易,但其中的 logger.py 模块可能要难一些,因为不运行 twisted 服务器就难以对它进行测试。
  • src/tests/test_locks_* —— 通常具有定义良好的状态以供测试。
  • src/tests/test_commands_* —— 这也比较容易测试,但cmdhandler可能是例外,因为它是异步的,而且使用了 twisted 的 deferred。还要注意,在默认命令中还有一些单独的测试。
  • src/tests/test_comms_* —— 对频道系统进行测试需要创建测试频道,并对它们执行操作。
  • src/tests/test_players_* —— 对创建玩家和删除玩家做测试,应该不算很难。
  • src/tests/test_objects_* —— 创建测试物体并对它们进行更改。为了测试 locations 和 destinations 之类的功能,你可能需要建立一组嵌套的物体(这需要用 TestCase 类的 SetUp() 方法来实现)。
  • src/tests/test_server_* —— 这可能是最复杂的测试用例,因为为了测试协议,需要为某些模块建立服务器实例及会话对象。


(原文:https://github.com/Evennia/evennia/wiki/Unit Testing    翻译:卢铱俊)