背景信息
- 我只使用 Django 工作了大约 3 个月。
- 至于 PyCharm,我使用它已经有几年时间了,但只是作为一个 IDE(像“PyCharm for dummies”一样),所以我没有深入研究其高级功能。
考虑到以上情况,对于一些高级用户来说,解决方案中的某些(或全部)部分可能会显得繁琐/愚蠢,请耐心等待。我将把任何能够增添价值的评论都纳入解决方案。
回到问题本身:我的测试/研究基于一个项目,该项目包括 Django 教程([DjangoProject]: Writing your first Django app)+ 一些来自 Django Rest Framework 教程 的部分([DRF]: Quickstart)。作为示例,我将尝试运行 polls/tests.py: QuestionViewTests.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函数),否则将正常运作。新实现包括:
- fileToMod从filePath中减去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字符并刷新stdout和stderr缓冲区(这是必需的,因为我注意到有时来自PyCharm和Django的输出会交错,最终结果会混乱)
- 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中,任何
input
(raw_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包中?)
- 我注意到helpers(utrunner的父级)文件夹中有很多特定于Django的文件/文件夹,也许那些也可以使用,将不得不检查
正如我在开始时所述,任何建议都非常欢迎!
@EDIT0:
- 正如我回复@Udi的评论所述,对于无法支付(或不愿意支付)PyCharm专业版许可费用的人来说,这是一个替代方案(快速浏览看起来每个实例大约是100$-200$ /年)
teamcity.django.TeamcityDjangoRunner
来解决。 - AndreyMZ