如何在PyCharm社区版中通过鼠标右键上下文菜单运行/调试Django应用程序的UnitTests?

9
我必须强调一下PyCharm社区版,它在问题时间(v2016.3.2没有任何Django集成
我已经通过谷歌搜索了我的问题,但令人惊讶的是,我没有得到任何答案(当然,我不排除可能有一些答案,但我只是错过了它们)。
问题很简单:在PyCharm中,可以通过右键单击(从上下文菜单中)来运行(调试)单元测试(TestCase或其方法),如下图所示: Run Django Unit Test at RClick 不幸的是,这会导致异常。
Traceback (most recent call last):
    File "C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py", line 254, in <module>
        main()
    File "C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py", line 232, in main
        module = loadSource(a[0])
    File "C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py", line 65, in loadSource
        module = imp.load_source(moduleName, fileName)
    File "E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py", line 7, in <module>
        from polls.models import Question
    File "E:\Work\Dev\Django\Tutorials\proj0\src\polls\models.py", line 9, in <module>
        class Question(models.Model):
    File "E:\Work\Dev\Django\Tutorials\proj0\src\polls\models.py", line 10, in Question
        question_text = models.CharField(max_length=200)
    File "E:\Work\Dev\VEnvs\py2713x64-django\lib\site-packages\django\db\models\fields\__init__.py", line 1043, in __init__
        super(CharField, self).__init__(*args, **kwargs)
    File "E:\Work\Dev\VEnvs\py2713x64-django\lib\site-packages\django\db\models\fields\__init__.py", line 166, in __init__
        self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE
    File "E:\Work\Dev\VEnvs\py2713x64-django\lib\site-packages\django\conf\__init__.py", line 53, in __getattr__
        self._setup(name)
    File "E:\Work\Dev\VEnvs\py2713x64-django\lib\site-packages\django\conf\__init__.py", line 39, in _setup
        % (desc, ENVIRONMENT_VARIABLE))
    django.core.exceptions.ImproperlyConfigured: Requested setting DEFAULT_INDEX_TABLESPACE, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
注意:我只是添加了这个问题,以便提供一个对某些人有用的答案。
2个回答

12
背景信息
  • 我只使用 Django 工作了大约 3 个月。
  • 至于 PyCharm,我使用它已经有几年时间了,但只是作为一个 IDE(像“PyCharm for dummies”一样),所以我没有深入研究其高级功能。

考虑到以上情况,对于一些高级用户来说,解决方案中的某些(或全部)部分可能会显得繁琐/愚蠢,请耐心等待。我将把任何能够增添价值的评论都纳入解决方案。

回到问题本身:我的测试/研究基于一个项目,该项目包括 Django 教程[DjangoProject]: Writing your first Django app)+ 一些来自 Django Rest Framework 教程 的部分([DRF]: Quickstart)。作为示例,我将尝试运行 polls/tests.pyQuestionViewTests.test_index_view_with_no_questions()

作为一条注释,将 DJANGO_SETTINGS_MODULE 设置为异常会触发另一个异常,以此类推...
第二步:创建 Python 配置。
尽管这不是问题的答案(只是较为相关),但我还是发表一下(我相信许多人已经这样做了):
  • 点击菜单Run -> Edit Configurations...
  • Run/Debug Configurations对话框中:
  • 添加一个类型为Python的新配置
  • Working directory设置为项目的根路径(对我来说是 "E:\Work\Dev\Django\Tutorials\proj0\src")。默认情况下,这也会将路径添加到Python的模块搜索路径中
  • Script设置为您的Django项目启动脚本(manage.py
  • Script parameters设置为测试参数(test QuestionViewTests.test_index_view_with_no_questions
  • 给您的配置命名(可选),然后点击OK。现在,您就可以运行此测试了
当然,对于每个测试用例(及其方法)都这样做并不是正确的方式(确实很烦人),因此这种方法是不可扩展的。
3. 调整PyCharm以实现我们想要的功能。
请注意,我不认为这是一个真正的解决方案,它更像是一种(无聊的)变通方法(gainarie),而且它也很具有侵入性。
让我们从查看当我们在“test”上右键单击时会发生什么开始(我将在一般情况下使用这个术语 - 它可能意味着测试用例、方法或整个测试文件,除非另有说明)。对我来说,它运行以下命令:
"E:\Work\Dev\VEnvs\py2713x64-django\Scripts\python.exe" "C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py" E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py::QuestionViewTests::test_index_view_with_no_questions true
如您所见,它正启动“C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py”(我将称之为utrunner),并使用一堆参数(第一个对我们很重要,因为它是测试规范)。utrunner使用的测试运行框架不关心Django(实际上有一些Django处理代码,但这对我们没有帮助)。
关于PyCharm运行/调试配置,简要说明如下:
  • PyCharm中右键单击一个test时,PyCharm会自动创建一个新的Run configuration(您可以保存它),就像您从Run/Debug Configurations对话框中创建一样。需要注意的一件重要的事情是配置类型,即Python tests/Unittests(它会自动启动utrunner)。
  • 通常情况下,当创建一个Run configuration时,PyCharm会从该配置类型Defaults(可以在Run/Debug Configurations对话框中查看)中“复制”设置到新的配置中,并使用特定数据填充其他设置。关于Default configurations的一个重要事项是它们是基于项目的:它们驻留在项目的.idea文件夹(workspace.xml)中,因此修改它们不会影响其他项目(这是我最初担心的)。

有了上述信息,让我们继续:

需要做的第一件事是:从“运行/调试配置”对话框(菜单:“运行 -> 编辑配置...”)中编辑“默认值/Python测试/单元测试”设置:
  • 设置“工作目录”,就像前面的方法一样
  • 在“环境变量”中添加一个名为“DJANGO_TEST_MODE_GAINARIE”的新变量,并将其设置为任何字符串(不为空/ null)
第二个步骤更加棘手(需要进行干预):修补“utrunner”。

utrunner.patch:

--- utrunner.py.orig    2016-12-28 19:06:22.000000000 +0200
+++ utrunner.py 2017-03-23 15:20:13.643084400 +0200
@@ -113,7 +113,74 @@
   except:
     pass

-if __name__ == "__main__":
+
+def fileToMod(filePath, basePath):
+  if os.path.exists(filePath) and filePath.startswith(basePath):
+    modList = filePath[len(basePath):].split(os.path.sep)
+    mods = ".".join([os.path.splitext(item)[0] for item in modList if item])
+    return mods
+  else:
+    return None
+
+
+def utrunnerArgToDjangoTest(arg, basePath):
+  if arg.strip() and not arg.startswith("--"):
+    testData = arg.split("::")
+    mods = fileToMod(testData[0], basePath)
+    if mods:
+      testData[0] = mods
+      return ".".join(testData)
+    else:
+      return None
+  else:
+    return None
+
+
+def flushBuffers():
+  sys.stdout.write(os.linesep)
+  sys.stdout.flush()
+  sys.stderr.write(os.linesep)
+  sys.stderr.flush()
+
+
+def runModAsMain(argv, codeGlobals):
+  with open(argv[0]) as f:
+    codeStr = f.read()
+  sys.argv = argv
+  code = compile(codeStr, os.path.basename(argv[0]), "exec")
+  codeGlobals.update({
+    "__name__": "__main__",
+    "__file__": argv[0]
+    })
+  exec(code, codeGlobals)
+
+
+def djangoMain():
+  djangoTests = list()
+  basePath = os.getcwd()
+  for arg in sys.argv[1: -1]:
+    djangoTest = utrunnerArgToDjangoTest(arg, basePath)
+    if djangoTest:
+      djangoTests.append(djangoTest)
+  if not djangoTests:
+    debug("/ [DJANGO MODE] Invalid arguments: " + sys.argv[1: -1])
+  startupTestArgs = [item for item in os.getenv("DJANGO_STARTUP_TEST_ARGS", "").split(" ") if item]
+  startupFullName = os.path.join(basePath, os.getenv("DJANGO_STARTUP_NAME", "manage.py"))
+  if not os.path.isfile(startupFullName):
+    debug("/ [DJANGO MODE] Invalid startup file: " + startupFullName)
+    return
+  djangoStartupArgs = [startupFullName, "test"]
+  djangoStartupArgs.extend(startupTestArgs)
+  djangoStartupArgs.extend(djangoTests)
+  additionalGlobalsStr = os.getenv("DJANGO_STARTUP_ADDITIONAL_GLOBALS", "{}")
+  import ast
+  additionalGlobals = ast.literal_eval(additionalGlobalsStr)
+  flushBuffers()
+  runModAsMain(djangoStartupArgs, additionalGlobals)
+  flushBuffers()
+
+
+def main():
   arg = sys.argv[-1]
   if arg == "true":
     import unittest
@@ -186,3 +253,10 @@

   debug("/ Loaded " + str(all.countTestCases()) + " tests")
   TeamcityTestRunner().run(all, **options)
+
+
+if __name__ == "__main__":
+  if os.getenv("DJANGO_TEST_MODE_GAINARIE"):
+    djangoMain()
+  else:
+    main()

上述是一个diff[man7]:DIFF(1))或者patch(这两个名称可以联合使用,但我更喜欢和使用patch),它显示了utrunner.py.orig(原始文件——在我开始修改之前保存的文件,您不需要这样做)和utrunner.py(包含更改的当前版本)之间的差异。我使用的命令是diff --binary -uN utrunner.py.orig utrunner.py(显然,在utrunner文件夹中)。作为个人评论,patch是改变第三方源代码的首选方式(以便控制更改并进行分离)。 patch中的代码所执行的操作(可能比普通的Python代码更难理解):
  • 主块下的所有内容(if __name__ == "__main__":或当前行为)已移动到名为main的函数中(以保持其分离并避免错误修改)
  • 修改了main块,因此如果定义了环境变量DJANGO_TEST_MODE_GAINARIE(且不为空),则会遵循新实现(djangoMain函数),否则将正常运作。新实现包括:
    • fileToModfilePath中减去basePath并将差异转换为Python包样式。例如:fileToMod("E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py", "E:\Work\Dev\Django\Tutorials\proj0\src"),将返回polls.tests
    • utrunnerArgToDjangoTest:使用前面的函数,然后添加类名(QuestionViewTests)和(可选的)方法名(test_index_view_with_no_questions),因此最终将测试规范从utrunner格式(E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py::QuestionViewTests::test_index_view_with_no_questions)转换为manage.py格式(polls.tests.QuestionViewTests.test_index_view_with_no_questions
    • flushBuffers:写入一个eoln字符并刷新stdoutstderr缓冲区(这是必需的,因为我注意到有时来自PyCharmDjango的输出会交错,最终结果会混乱)
    • runModAsMain:通常,所有相关的manage.py代码都在if __name__ == "__main__":下。此函数“欺骗”Python,使其相信manage.py是其第一个参数
修补 utrunner:
  • 我自己进行了这些修改(我没有搜索带有Django集成并从中获得启发的版本)
  • utrunner是PyCharm的一部分。显然,JetBrains的人们为什么没有在Community Edition中包含任何Django集成:为了让人们购买Professional Edition。这有点伤害了他们的利益。我不知道修改utrunner的法律影响,但是无论如何,如果您打了补丁,您需要自行承担责任和风险
  • 编码风格:它很糟糕(至少从命名/缩进的角度来看),但它与文件的其余部分保持一致(唯一的情况是编码风格应该允许糟糕)。[Python]: PEP 8 -- Style Guide for Python Code 包含了Python的编码风格指南
  • 该补丁应用于原始文件(utrunner.py),具有以下属性(对于v2019.2.3仍然有效(最后检查:20190930)):
    • 大小:5865
    • sha256sum:db98d1043125ce2af9a9c49a1f933969678470bd863f791c2460fe090c2948a0
  • 应用补丁:
    • utrunner位于“${PYCHARM_INSTALL_DIR}/helpers/pycharm”中
    • 通常,${PYCHARM_INSTALL_DIR}指向:
      • Nix:/usr/lib/pycharm-community
      • Win:“C:\Program Files (x86)\JetBrains\PyCharm 2016.3”(根据您的版本号进行调整)
    • 保存补丁内容(在名为utrunner.patch的文件中,假设它位于/tmp下)
    • Nix-事情很容易,只需(cd到utrunner的文件夹并)运行patch -i /tmp/utrunner.patch[man7]: PATCH(1) 是一个默认安装的实用程序(在Ubtu的patch dpkg的一部分)。请注意,由于utrunner.py归root所有,因此在此步骤中,您需要sudo
    • Win-要按照类似的步骤进行操作,但是事情会更棘手,因为没有本地的补丁实用程序。但是,有解决方法:
      • 使用Cygwin。与Nix(Lnx)情况一样,在Cygwin安装中可用补丁实用程序,但是它不会默认安装。必须从Cygwin设置中明确安装补丁pkg。我尝试过这个,它可以工作
      • 有替代方案(我没有尝试过它们):
      • 与Nix的情况一样,打补丁文件(很可能)必须由其中的一个管理员完成。此外,请注意文件路径,如果包含空格,请确保将其引用起来
    • 还原补丁:
      • 备份对我们的情况没有害处(除了从免费磁盘空间的角度来
        关于这种方法,简单说几句:
        该代码可以处理(可选的)环境变量(除了强制要求的之外):
        - :在manage.py具有其他名称(出于任何原因?),或位于另一个文件夹而不是工作目录的情况下使用。这里很重要的一点是:当指定文件路径时,请使用特定于平台的路径分隔符:斜线/)表示Nix,反斜杠(\)表示Win。 - :额外的参数,manage.py测试接受(运行manage.py test --help以获取整个列表)。在这里,我必须坚持使用-k/--keepdb,它保留测试数据库(默认情况下为test_${REGULAR_DB_NAME}或在设置下的TEST字典中设置)在运行之间。对于运行单个测试,创建DB(并应用所有迁移)并销毁它可能需要耗费时间(并且非常烦人)。此标志确保在结束时不删除DB,并将在下一次测试运行时重复使用。 - :这必须具有Python dict的字符串表示形式。任何由于某种原因需要在globals()字典中存在的值,都应放置在此处。
        当修改默认配置时,所有先前创建的继承它的配置都不会更新,因此必须手动删除它们(并将通过对其进行新的右键单击来自动重新创建它们的测试)。
        在删除先前的配置后,右键单击相同的测试,然后就完成了:voilà。
        E:\Work\Dev\VEnvs\py2713x64-django\Scripts\python.exe "C:\Install\PyCharm Community Edition\2016.3.2\helpers\pycharm\utrunner.py" E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py::QuestionViewTests::test_index_view_with_no_questions true
        Testing started at 01:38 ...
        
        
        Using existing test database for alias 'default'...
        .
        ----------------------------------------------------------------------
        Ran 1 test in 0.390s
        
        OK
        
        Preserving test database for alias 'default'...
        
        
        Process finished with exit code 0
        
        调试也可以使用断点等方式。 注意事项(目前我已经确定了两个):
        • 这是一件良性问题,只是一个UI问题: utrunner(很可能)有一些初始化,PyCharm期望在我们的情况下发生,但显然没有发生。因此,即使测试成功结束,从PyCharm的角度来看,它们也没有结束,因此输出窗口将包含一个警告:““测试框架意外退出
        • 这是一个讨厌的问题,我还没有找到根本原因。显然,在utrunner中,任何inputraw_input)调用都不能很好地处理;提示文本:“如果您想尝试删除测试数据库'test_tut-proj0',请输入“yes”,或者输入“no”取消:”(如果上一个测试运行崩溃,并且其DB未在最后被销毁)不会被显示,并且程序会冻结(这在utrunner之外不会发生),而不允许用户输入文本(也许有线程混合在其中?)。唯一的恢复方式是停止测试运行,删除DB并重新运行测试。同样,我必须推广manage.py test -k标志,以解决此问题
        我曾在以下的环境中工作/测试过:
        • Nix (Lnx):
          • Ubtu 16.04 x64
          • PyCharm社区版2016.3.3
          • Python 3.4.4 (VEnv)
          • Django 1.9.5
        • Win:
          • W10 x64
          • PyCharm社区版2016.3.2
          • Python 2.7.13 (VEnv)
          • Django 1.10.6

        • 我将继续调查当前问题(至少第二个)
        • 一个干净的解决方案是在PyCharm中以某种方式覆盖单元测试运行的默认设置(我从代码中实现了这一点),但我找不到任何配置文件(可能在PyCharm的jar包中?)
        • 我注意到helpersutrunner的父级)文件夹中有很多特定于Django的文件/文件夹,也许那些也可以使用,将不得不检查

        正如我在开始时所述,任何建议都非常欢迎!

        @EDIT0

        • 正如我回复@Udi的评论所述,对于无法支付(或不愿意支付)PyCharm专业版许可费用的人来说,这是一个替代方案(快速浏览看起来每个实例大约是100$-200$ /年)

6
购买PyCharm专业版吧,它有很多适用于Django的优秀功能。 - Udi
这是真的(或者至少我在截图上看到了),但对于一些人来说,这是一个替代方案,因为他们可能无法支付每年100美元至200美元的费用(或者一些公司根本拒绝支付这些金额)。 - CristiFati
第一个问题可以通过使用来自 teamcity-messages 的测试运行器 teamcity.django.TeamcityDjangoRunner 来解决。 - AndreyMZ

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接