用PyCharm的交互式调试器为Django写更好的测试

485 阅读9分钟

PyCharm的交互式调试器如何成为我开发过程中的宝贵部分,以及它如何也应该成为你的一部分。

Cover

PyCharm的交互式调试器将帮助你更快地写出更健壮的代码,帮助你理解工作流程,更容易发现和调试代码中的错误。

在这篇文章中,我们将通过在PyCharm上设置一个基本的Django项目。我们将安装pytest,并告诉你如何启用它作为默认的测试运行器。最后,我们将告诉你如何使用调试器来发现和修复一些错误,以及如何在日常开发工作流程中使用它。最后还有一个奖励,所以请坚持下去。

项目设置

本文中的所有内容都可以用PyCharm的社区版来完成,所以请到他们的网站上为你的系统下载免费版本。如果你遵循默认的安装说明,你应该在几分钟内准备好开始工作。我们显然需要一个项目来工作,我已经建立了一个项目,你可以从Github上下载。这个项目是基于Writing your first Django app教程的,你可以在他们的网站上找到。如果你是一个初学者,或者想补习一些基础知识,我强烈建议你跟着这个教程走。我也将很快上传另一篇关于编写和发布自己的可重用Django包的文章。

首先从版本库中克隆代码。现在我们已经准备好了我们的编辑器和代码,让我们开始在PyCharm上设置我们的项目。

1.打开你的项目

使用顶部导航在PyCharm中打开你的项目。

Open menu navigation

导航到你克隆项目的位置,在一个新窗口中打开基本项目文件夹。你应该看到以下内容。

Your first django app in new pycharm window

你的第一个Django App第一次打开时的样子。

打开项目后,我们可以开始为我们的项目配置设置。

2.创建或使用一个虚拟环境

默认情况下,PyCharm会使用你所拥有的任何python解释器,然而你很可能总是希望至少在一个虚拟环境中运行你的项目。你可以直接从设置(偏好)窗口创建一个,或者添加一个现有的。

我使用pyenv,因此我将添加我在项目设置过程中创建的现有环境,但你可以去点击右上方的齿轮齿轮,创建一个新环境或添加一个你自己的环境。注意,你要把你的python解释器改为python3。

New python3 virtual environment

创建了新的虚拟环境后,你现在应该看到类似这样的东西。

New virtual environment without django installed yet.

这里你会看到默认安装在你的虚拟环境中的软件包。你会注意到,Django还没有被安装。我们现在要解决这个问题。

你可以直接从这里安装软件包,也可以从命令行直接在虚拟环境中安装它们。我们将使用命令行,因为我们也想把我们的软件包添加到我们项目的requirements.txt文件中,以跟踪我们将要使用的东西。

将以下requirements.txt文件添加到你项目的基本目录中。

Django==4.0.4
pytest==7.1.2
pytest-django==4.5.2

你可以用pip install -r requirements.txt安装需求,这将安装你所有的需求。

3.设置测试运行器

为了使用pytest包来运行我们的测试,我们还需要改变一个配置设置。导航到工具>Python集成工具,设置默认的测试运行器为pytest。

Configure pytest as the default test runner

我们还需要告诉pytest在哪里可以找到我们的项目设置文件。所以我们在项目中添加以下pytest.ini文件。

[pytest]
DJANGO_SETTINGS_MODULE=mysite.settings
python_files = tests.py test_*.py *_tests.py

很好,现在我们的项目已经设置好了,我们现在可以开始写一些代码和使用交互式调试器了。

编写我们的第一个测试

以纯粹的TDD方式,在我们做任何事情之前,我们将编写我们的第一个TestCase。在polls/tests.py文件中添加以下代码。

from django.test import TestCase
from django.urls import reverse


class IndexViewTests(TestCase):
    def test_index(self):
        """
        Test the index page.
        """
        response = self.client.get(reverse('polls:index'))
        assert response.status_code == 200

现在你可以在PyCharm用户界面中导航到你的测试,右击test_index并选择运行 "pytest for tests.IndexViewTests.test_index"。这将运行你的单个测试。你可以对测试类或整个test.py文件做同样的操作。

Run a single test using pytest in django

现在你会看到测试失败了,在下面的控制台中会有一些输出。

Single testcase no-reverse-match error

现在我们可以使用我们的错误输出来修复我们的测试。由于我们没有在Django中定义视图或url,我们应该知道这就是为什么我们的测试会出现NoReverseMatch异常而失败。幸运的是,我们可以通过添加索引页的视图和url映射来快速解决这个问题。

# polls/views.py
from django.http import HttpResponse


def index(request):
    response_text = "Hello, world. You're at the polls index."
    return HttpResponse(response_text)

和民意调查应用的url

# polls/urls.py
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
]

然后在mysite/urls.py中把pollls App的urls添加到网站的urls中。

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

你可以通过使用PyCharm中的alt+return快捷键来添加include导入。将鼠标悬停在缺失的导入上,然后按alt+return,它应该提示你是否要添加导入。请确保选择正确的导入。

alt+return shortcut for adding imports.

再次运行我们的测试,它现在应该通过了。现在我们有了我们的项目和一个正在运行的测试,我们可以开始玩PyCharm中的交互式调试器了。

交互式调试器

我们现在可以使用调试器来检查我们所写的代码。为了使用调试器,我们需要在PyCharm的调试模式下运行测试,并在我们的代码中添加一个或多个断点。你可以通过点击行号旁边的沟槽在你的视图代码中添加一个断点。

Breakpoint in the line number gutter

现在使用调试模式运行测试。

Run the test in debug mode

测试现在将在断点处暂停执行。你会看到,函数中的变量的预览将显示在通往断点的行中。

Paused execution in view at the breakpoint.

然而,最有价值的是下面的交互式窗口,你可以进一步检查变量,并通过其他各种执行选项继续执行。在这个视图中还可以设置其他配置和视图。我们将在下文中介绍这些。

Interactive debugger view with execution options highlighted

PyCharm的文档中包含了大量的调试时可用的各种功能的例子,我将只阐述我最常使用的那些。

  1. 跨越
  2. 步入我的代码
  3. 评估表达式

在我们深入研究这些之前,让我们先跳过Django教程,以便我们有一个更好的测试来进行调试。从资源库中查看第5部分的标签。

使用我们之前使用的右键运行test_was_published_recently_with_future_question测试,我们看到测试如预期般失败。

Failing was published recently test

看一下测试中发生了什么。

def test_was_published_recently_with_future_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is in the future.
    """
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)

test_was_published_recently_with_future_question创建了一个Question实例,其pub_date字段是未来的30天,使用assertIs()方法,它发现其was_published_recently()返回True,尽管我们希望它返回False。现在我们可以利用我们的交互式调试器来找出到底是什么地方出了问题。

在测试的第一行添加一个中断点。我们将使用Step overStep into动作来找到故障点,我们将使用Evaluate表达式功能来查看哪里出了问题。继续在调试模式下运行该测试。你应该在这里结束。

Debug run of failing test

使用Step over动作,向下移动到assertIs语句。你将看到,你可以检查你所创建的时间确实是你所期望的,甚至它在你的future_question对象上被正确地设置。

Check pub_date and time values

现在我们知道这些都是正确的,我们可以跳到我的代码中 ,看看was_published_recently()函数中发生了什么。你应该在这里找到自己。

Debug of was published recently method

我们现在可以使用Evaluate表达式函数来找到并修复这个错误。如果我们在返回语句上运行Evaluate表达式,我们看到它返回的是True,尽管我们希望它是False。

Evaluate expression

进一步检查我们正在比较的日期,我们看到pub_date确实是在未来,但是我们没有从表达式中排除未来日期的帖子。我们需要检查pub_date是否也小于now()。我们可以在表达式评估中测试这一点,然后再把它放到我们的代码中。

Evaluate the corrected expression

很好,现在它似乎在工作,我们可以纠正我们的代码并重新运行我们的测试。这一次,它正确地通过了。记住,你可以在调试模式下的测试执行过程中随时使用恢复暂停停止按钮。

总结

希望通过这个简单的例子能帮助你理解这个过程,并将其应用于你的具体案例。它确实能帮助我更快地发现问题,特别是在处理不是我自己写的或很久之前写的代码时。使用Step into my code功能也有助于找到信号内部有时会发生问题的地方。

奖金 NO MIGRATIONS 🎉

当你在测试开始前有很多迁移时,或者当你想测试一个模型的变化而不经历整个迁移过程时,--nomigrations标志是一个救星。你可以在你的测试配置中设置这个标志。

edit test configurations

然后在Additional Arguments选项中添加--nomigrations参数

Configuring — nomigrations

或者你可以在你的pytest.ini文件中全局地添加它,但不要提交这个选项,因为你不希望你的CI在不需要进行任何迁移的情况下运行你的测试,如果你需要额外的变化,就需要回滚或改变它们。

[pytest]
DJANGO_SETTINGS_MODULE=mysite.settings
python_files = tests.py test_*.py *_tests.py
addopts = --nomigrations

继续往前走,在你的问题模型中添加一个非空字段,看看你的测试如何运行。

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    published_by = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.question_text

同时更新测试。

def test_was_published_recently_with_future_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is in the future.
    """
    user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time, published_by=user)
    future_question.save()
    self.assertIs(future_question.was_published_recently(), False)
    self.assertEqual(future_question.published_by, user)

我希望这能帮助你提高你的效率和你的代码的质量!