Django单元测试入门教程

219 阅读3分钟

Django单元测试入门

测试是软件开发过程中的一个重要步骤。许多软件开发者忽视了这一步,而手动测试他们的代码。

随着应用程序规模的扩大,对代码的手动测试变得很乏味。单元测试可以确保你添加到应用程序中的每一个组件都能按预期工作,而不会破坏其他功能的功能。

本教程将通过一个简单的Django应用程序,让学校管理员存储被录取的学生的信息。我们将为应用程序中的各个组件编写单元测试。

前提条件

  1. 在你的电脑上安装Python
  2. Django框架的了解。

项目设置

  1. 通过执行下面的命令创建一个新的Django项目。
$ django-admin startproject djangotesting
  • 上述命令生成了一个名为djangotesting 的Django应用程序。
  1. 一个Django项目通常被组织成应用程序。这使得管理大型项目变得很容易。让我们通过执行下面的命令在我们的项目中创建一个新的Django应用程序。
$ python manage.py startapp testing
  1. 通过执行下面的命令,将django restframework 添加到该应用程序中。
$ pip install djangorestframework
  • 一旦在我们的应用程序中安装了djangorestframework ,我们需要将其添加到INSTALLED_APPS 列表中的settings.py 文件,如下图所示。
# code
INSTALLED_APPS = [
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'testing.apps.TestingConfig',
 'rest_framework'
]   

模型

models.py 文件中,添加下面的代码片断。

# models.py
class Student(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    reg_number = models.CharField(max_length=50)
    date_of_admission = models.DateField(null=True, blank=True)

    def get_absolute_url(self):
        return reverse("student-detail", args=[str(self.id)])

    def __str__(self):
        return f"Name: {self.first_name} {self.last_name}"

上面的代码片断包含一个在数据库中创建的学生模型,作为一个学生表。该模型有两个方法:

  1. get_absolute_url(self): 返回一个特定学生详细页面的URL。
  2. __str__(self): 返回一个包含学生的姓和名的字符串。

序列化器

testing 应用程序中,创建一个名为serializers.py 的新Python文件,并添加以下代码。

# serializers.py
class StudentSerializer(ModelSerializer):
     class Meta:
         model = Student
         fields = "__all__"        

上面的代码片段将Student 模型转换为JSON,反之亦然。JSON数据很容易通过HTTP传输。这就是为什么数据被转换为JSON。

查看

views.py 文件中,添加下面的代码片断。

class StudentListView(generic.ListView):
     model = Student
     paginate_by = 10 # the number of students to return in each page
        
        
class StudentView(generic.DetailView):
     model = Student      

上面的代码片断有两个视图类:-

  1. StudentListView 返回学生的列表。
  2. StudentView 返回有关学生的详细信息。

API视图

testing 应用程序中创建一个新的名为api_views.py 的python文件,并添加下面的代码片断。

# api_view.py
class CreateStudentApiView(generics.CreateAPIView):
     queryset = Student.objects.all()
     serializer_class = StudentSerializer
        

上面的代码片断包含一个类,允许通过REST API创建学生。

URLs

testing 应用程序中,创建一个名为urls.py 的新的python文件,并添加下面的代码片段。

# testing/urls.py
urlpatterns = [
 path('students', StudentListView.as_view(), name="students"),
 path('students/create', CreateStudentApiView.as_view(), name="create-student"),
 path('students/<int:id>', StudentView.as_view(), name="student-detail")
]

上述代码片断包含了应用程序中各种视图的路径。

请确保更新根项目urls.py 文件中的testing 应用程序的URLs,如下所示。

# djangotesting/urls.py
urlpatterns = [
 path('admin/', admin.site.urls),
 path('', include('testing.urls'))
]

模板

  1. 在项目目录中,创建一个名为templates 的新目录。
  2. 在上面创建的templates 目录中,创建另一个名为testing 的目录。这将放置testing 应用程序的模板文件。
  3. 在模板目录下的testing 目录中创建一个新的HTML文件,名为student_list.html ,并添加下面的代码片段。
<!-- testing/student_list.html -->
<!doctype html>
<html lang="en">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <title>Hello, world!</title>
</head>

<body>
    <div class="container" style="margin-top: 100px;">
        <table class="table">
            <thead class="thead-dark">
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">First</th>
                    <th scope="col">Last</th>
                    <th scope="col">Reg. No.</th>
                </tr>
            </thead>
            <tbody>
                <!-- prints out the students details in a table -->
                {% for student in student_list %}
                <tr>
                    <th scope="row">{{ student.id }}</th>
                    <td>{{ student.first_name }}</td>
                    <td>{{ student.last_name }}</td>
                    <td>{{ student.reg_number }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
        crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
        integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
        integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
        crossorigin="anonymous"></script>
</body>

</html>

运行该应用程序,通过执行下面的命令来确保一切工作正常。

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

编写单元测试

我们将从为我们的视图编写测试开始。在testing 应用程序中,创建一个名为tests 的新的python包。

测试视图

在上面创建的tests 包中,创建一个新的名为tests_views.py 的python文件。

注意这是一个惯例,测试文件应该以test这个词开头。

将下面的代码片段添加到上面创建的tests_views.py 文件中。

### test_views.py
class StudentListViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        number_of_students = 30
        for student_id in range(number_of_students):
            Student.objects.create(first_name=f"John{student_id}", last_name=f"Doe{student_id}")

    def test_url_exists(self):
        response = self.client.get("/students")
        self.assertEqual(response.status_code, 200)

    def test_url_accessible_by_name(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)

    def test_view_uses_correct_template(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'testing/student_list.html')

    def test_pagination_is_correct(self):
        response = self.client.get(reverse('students'))
        self.assertEqual(response.status_code, 200)
        self.assertTrue('is_paginated' in response.context)
        self.assertTrue(response.context['is_paginated'] is True)
        self.assertEqual(len(response.context['student_list']), 10)
  • setUpTestData(cls) 方法被标记为 ,因为当类被执行时,它首先被执行。在这个函数中,我们创建了存储在临时测试数据库中的学生对象,并在整个测试类中使用。@classmethod
  • test_url_exists 方法向提供的路径发出HTTP请求,并检查结果代码是否成功。
  • test_url_accessible_by_name 方法从给定的名称中构建一个URL,并对创建的URL进行HTTP请求,然后检查请求的状态码。
  • test_view_uses_correct_template 方法检查在访问指定路径时是否加载了正确的模板。
  • test_pagination_is_correct 方法测试返回的数据是否是分页的。

测试模型

test 包中,创建一个名为test_models.py 的新文件并添加以下代码片段。

# test_models.py
class StudentModelTestcase(TestCase):
    @classmethod
    def setUpTestData(cls):
        Student.objects.create(first_name="Peter", last_name="John", reg_number="111b2")

    def test_string_method(self):
        student = Student.objects.get(id=1)
        expected_string = f"Name: {student.first_name} {student.last_name}"
        self.assertEqual(str(student), expected_string)

    def test_get_absolute_url(self):
        student = Student.objects.get(id=1)
        self.assertEqual(student.get_absolute_url(), "/students/1")

在上面的测试代码中:

  • setUpTestData 方法设置了将在整个测试类中使用的对象。
  • test_string_method 方法测试从 模型的 方法返回的字符串是否有效。Student __str__
  • test_get_absolute_url 方法测试从模型返回的绝对URL是否有效。

测试API视图

tests 包中,创建一个名为tests_api_view.py 的新的Python文件,并添加下面的代码片段。

# test_api_view.py
class StudentSerializerTestCase(APITestCase):
    def student_creation_test(self):
        payload = {
            "first_name": "Joan",
            "last_name": "Keith",
            "reg_number": "Abrt1",
            "date_of_admission": datetime.date.today()
        }
        response = self.client.post(reverse("student-create"), payload)
        self.assertEqual(status.HTTP_201_CREATED, response.status_code)      

在上面的代码片断中,我们只有一个方法:

  • student_creation_test 来测试学生创建端点。在该方法中,我们创建一个包含创建学生所需的所有数据的有效载荷,然后用该有效载荷向 端点发出一个POST请求。students/create

注意在test_api_view中我们使用了来自restframework的APITestCase ,而不是使用来自django的TestCase

要运行我们的测试,请在当前工作目录下的终端执行以下命令。

$ python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
../home/quinter/PycharmProjects/djangoTest/venv/lib/python3.8/site-packages/django/views/generic/list.py:86: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'testing.models.Student'> QuerySet.
  return self.paginator_class(
----------------------------------------------------------------------
Ran 6 tests in 0.044s

OK
Destroying test database for alias 'default'...

从上面的结果中,我们可以看到,我们的六个测试都运行了,而且都通过了。

总结

现在你已经学会了如何为Django应用程序中的各种组件编写单元测试,为我们的书生入学应用程序创建一个RESTful端点,并为序列化器和API视图添加单元测试。