如果你是一个开发人员,你已经知道测试在任何软件项目中是多么重要。特别是自动测试,因为它可以帮助你证实你的编码,并迅速看到你的程序做了什么你想做的事。它还可以帮助证实你的代码中的任何新的变化,没有破坏任何以前的功能。
说到这里,这是否意味着,如果你有自动测试,那么你的项目就不会有任何错误?并非如此。正如计算机科学家Edsger W. Dijkstra曾经说过:
“Program testing can be used to show the presence of bugs, but never to show their absence”.
这句话帮助我们理解,我们不能确定一个程序是完美的,但是,测试是帮助我们发现错误并进行修复和改进的根本。作为一个从事软件开发的人,我可以说,当你发现一个错误时,比你的客户或生产中的用户要好得多。在这篇文章中,我将谈论自动测试的基本知识,有用的工具,以及Django项目的良好实践,重点是API。
Django和Django REST框架的测试
Django有一个非常好的测试文档,Django REST框架也是如此。因此,在这篇博客中,我将谈论这两个框架中的主要工具,以及你可以做什么和用什么来改善你的测试。
主要的测试类
Django为测试提供了几个类。我在这里要谈的是TestCase,如果你的应用程序使用了数据库,它就非常有用。
正如其名,要在你的Django项目中创建一个测试案例,你将定义一个继承自TestCase的类。通过这样做,你可以使用该类的所有方法和属性,这将有助于你创建和执行你的测试。然后,你将定义不同的函数,对应于测试案例中的每个单元测试。让我们来看看一个测试用例的结构的例子:
# your test file
from django.test import TestCase
# class to define a test case for login
class UserLoginTestCase(TestCase):
...
# some setup here, explained later
def test_correct_login(self):
# unit test
# Corroborate the expected scenario
...
def test_if_password_incorrect_then_cant_login(self):
# unit test
# Corroborate that user's password needs to be only the correct one
...
def test_if_user_not_registered_cant_login(self):
# unit test
# Corroborate that user's are able to login only if they're registered
...
另外,你可以使用Django的客户端类,它模拟了一个假的浏览器,所以你可以进行HTTP请求,并测试你的Django API如何响应。
由于我们专注于API测试,我想谈谈APITestCase,这是Django REST框架的一个类,是TestCase的镜像,但使用不同的客户端类:APIClient。这个客户端扩展了TestClient,意味着它具有相同的功能,并增加了其他的功能,如credentials 功能。这个函数对于覆盖认证头信息非常有用,例如,使用OAuth1、OAuth2或任何简单的令牌认证方案。
所以,如果你正在做一个使用Django REST框架的项目,你可以用这种方式改变前面的例子:
# your test file
from rest_framework.test import APITestCase
# class to define a test case of login
class UserLoginTestCase(APITestCase):
...
主要测试功能
前面提到的类都有一组方法,可以帮助你完成测试工作。通过这些函数,你能够证实你的软件按照预期工作,并制作必要的测试夹具。
测试夹具是一个环境,你可以使用一致性来创建运行你的测试。使用固定装置,你可以确保在执行前满足某些条件。例如,在你的测试数据库中有一个确定的数据集,或者创建需要的对象。下面是一个主要的列表:
setUp和tearDown: 这些是在每个单元测试之前(setUp)和之后(tearDown)要执行的函数。这些对固定装置非常有用。setUpClass和tearDownClass: 与 setUp 和 tearDown 类似,这些函数在整个测试用例中在(setUpClass)和(tearDownClass)之后执行。这意味着它对一个测试用例只执行一次。因为它们被Django用来做重要的配置,如果你在你的测试用例类中覆盖了这些方法,别忘了在函数里面调用super实现。setUpTestData: 这个函数可以用来有一个类级的原子块来定义整个测试案例的数据。这意味着在测试用例的所有单元测试最终完成后,这个函数会自动回滚数据库中的变化。这主要是用来加载数据到你的测试数据库。- 断言方法。Django有一套来自unittest的断言方法,并有另一个为该框架创建的方法。这些方法,如
assertEqual,assertIsNone,assertTrue, 等,可以在单元测试中用来检查必须满足的不同条件。例如,在登录的单元测试中,你可以断言响应的状态代码为200,因为你要确保如果数据被正确发送,那么你的应用程序必须返回带有该代码的响应。
你可以避免使用这些函数,或者使用它们的任何组合。它们不是必需的,但它们非常有用,会对你有很大的帮助。
测试发现与数据库
你必须把你的测试类放在哪里?默认情况下,Django会识别当前工作目录下任何符合模式的文件test*.py 。也就是说,目录下任何以test 开头的文件,当然也有.py 的扩展名。在里面,Django会执行测试案例类中以test 开始的任何函数。
当你运行你的测试时,你可以传递一个参数表示所需的模式,以防你想使用一个不同的模式。有了这种行为,放置测试用例的地方可以是任何地方,但我建议将测试分为你项目中可能有的几个Django应用程序。例如,如果你有一个名为users 的 Django 应用,其中有与你项目中的用户相关的模型和功能,你可以在其中定义一个test 文件夹作为模块(添加init.py 文件),其中包含所有涵盖该重要部分的测试:对登录、注册、用户数据、账户删除等的测试。类似这样的东西:
my_project
|__django_apps
|__users
|__test
|__ __init__.py
|__ test_login.py
|__ test_signup.py
|__ test_delete_user.py
...
...
...
...
关于数据库,当你默认运行你的测试时,Django会创建一个数据库,然后在最终完成后将其销毁。这是为了避免你的生产和/或开发数据库,以及你的测试数据库之间的冲突。你可以为测试定义和定制一个数据库,在DATABASES 字典中,添加另一个名为TEST 的数据库。类似的情况:
DATABASES = {
'default': {
'ENGINE': '<engine>',
'NAME': '<database name>',
'USER': '<database user>',
...
},
...
'TEST': {
# testing database customization here
'NAME': '<your testing database name>',
# some other customization, for example the user user, password, etc
},
}
看一下TEST 字典的可用键。如果你不定义它,Django会创建一个数据库,将其命名为与你在default 设置中的数据库相同,附加上_test 的后缀。
有用的工具
在本节中,我将谈论两个对固定装置非常有帮助的外部工具:Faker和Factory Boy。
Faker
Faker是一个Python包,用于生成虚假但真实的数据。你可以使用它的功能将数据加载到你的测试数据库中,为你要测试的请求生成数据,并为模型生成数据等。Faker有一个巨大而多样的可能性。你可以在执行时生成,例如,名字、姓氏、电话号码、日期、密码、电子邮件等。
此外,你可以向函数传递参数以生成具有不同约束条件的数据,例如,你可以获得一个密码,指定是否要使用特殊字符,所需的长度,以及是否要有大写字母,等等。看看Faker提供者,看看所有不同的虚假但真实的数据,你可以为你的测试生成。而不是使用例如user_test@mail.com ,你的测试案例可以有更接近现实的数据,提高质量。
Factory Boy
如果你在测试中使用工厂,你可以在一个类中定义你想在测试数据库中拥有的数据结构。然后在你的测试中,将更容易拥有你的模型的实例,并以所需的结构加载数据。Factory Boy是一个使用工厂来取代静态的、难以维护的固定装置的工具。你也可以把它和Faker结合起来,让工厂拥有虚假但真实的数据。让我们看一个例子。
在这个例子中,我们可以看到一个Django的静态夹具,在一个叫做Person的模型中加载数据到测试数据库,这个模型有一个名字和姓氏。这个JSON文件定义了两个实例:
[ { "model": "myapp.person", "pk": 1, "fields": { "first_name": "John", "last_name": "Lennon" } }, { "model": "myapp.person", "pk": 2, "fields": { "first_name": "Paul", "last_name": "McCartney" } }]
现在我们来看看使用Factory Boy和Faker的类似定义:
from factory import django, Faker
from myapp import models
class PersonFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Person
first_name = Faker('first_name')
last_name = Faker('last_name')
有了这个,你可以使用Factory Boy提供的函数,例如,PersonFactory.create_batch(2) ,在你的测试数据库中即时创建2个具有不同的、现实的姓和名的Person的实例。在JSON情况下,如果你想有10个实例,你将需要添加它们并明确定义新的名字和姓氏。如果你想拥有巨大的数据集,JSON文件将是巨大的,难以维护,而在工厂案例中,你只需要调用一个函数。另外,如果模型中的Person发生变化怎么办?在JSON情况下,你必须逐一更新每个定义的实例。而在工厂化的情况下,你只需要改变一个类的定义。Factory Boy的文档中有一节叫做普通配方,你可以从库中找到有用的工具、实践和提示。下面是一个有趣的功能列表:
- 你可以使用
RelatedFactory或SubFactory类创建包括模型关系的模型工厂。 - 使用
create函数在工厂中创建一个新的模型实例并将其保存到测试数据库中。如果你想拥有实例,但不把它们保存到数据库中,可以使用build来代替。 - 以类似的方式,你可以使用
create_batch(N)创建工厂中模型的 N 个实例并将其保存到数据库中,如果你不想保存它们,可以使用build_batch(N)。 - 你可以在工厂中定义一个属性,在一组选择中选择一个,就像Django模型的选择字段,使用Faker的
random_element。在下一节,我们将看到一个这样的例子。
测试实例与良好实践
在这一节中,我想总结并展示一个小例子。这就是小小的现实。在这个项目中,我们有一个名为用户的Django应用,在这里我们有用户模型,它有一个用户名、电话和系统中的一个类别,可能是管理员、普通用户和访客。用户可以注册并登录到该应用中。
模型和工厂的定义
# model.py in the Django app called users
from django.db import models
from django.contrib.auth.models import AbstractUser
# class to define choices
class Category(models.TextChoices):
GUEST = 'G', 'Guest user'
COMMON_USER = 'C', 'Common user'
ADMIN = 'A', 'Admin user'
# user model that inherits from AbstractUser
# there is no need to define a username field
# because is defined in the parent class
class User(AbstractUser):
phone_number = models.CharField(max_length=50, blank=True, null=True)
category = models.CharField(
max_length=1,
choices=Category.choices,
default='G',
)
现在,让我们看看用户工厂的实现。这个文件在用户Django应用程序的一个测试文件夹内:
# factory.py inside users/test
from faker import Faker as FakerClass
from typing import Any, Sequence
from factory import django, Faker, post_generation
from users.models import User, Category
CATEGORIES_VALUES = [x[0] for x in Category.choices]
class UserFactory(django.DjangoModelFactory):
class Meta:
model = User
username = Faker('user_name')
phone_number = Faker('phone_number')
category = Faker('random_element', elements=CATEGORIES_VALUES)
@post_generation
def password(self, create: bool, extracted: Sequence[Any], **kwargs):
password = (
extracted
if extracted
else FakerClass().password(
length=30,
special_chars=True,
digits=True,
upper_case=True,
lower_case=True,
)
)
self.set_password(password)
关于这一点,有几件事:
- 在工厂内部的
Meta类中,我们必须指出相应的模型。 - 在类别属性中,我们使用Faker的
random_element,从类别选择列表中随机选择一个。我们定义了CATEGORIES_VALUES,以指定我们要取哪一部分的选择。这是因为选择列表中的每个元素都是一个元组,其中第一部分是值,第二部分是解释性的文本。所以,那个CATEGORIES_VALUES集的每个元素都只有那个第一部分。 - 通过
post_generator装饰器和password函数,我们表明在创建一个用户实例的时候,我们可以传递一个想要使用的密码,或者使用Faker功能生成一个。
测试案例
现在,让我们看看用户注册功能的测试实例。这个例子项目的认证部分是用dj-rest-auth实现的。我就不谈这个了,如果你想了解更多,请看一下这个博客。这是用户Django应用程序的测试文件夹内的test_singup.py文件:
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from users.test.factory import UserFactory
from users.models import User, Category
from faker import Faker
class UserSignUpTestCase(APITestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_object = UserFactory.build()
cls.user_saved = UserFactory.create()
cls.client = APIClient()
cls.signup_url = reverse('rest_register')
cls.faker_obj = Faker()
def test_if_data_is_correct_then_signup(self):
# Prepare data
signup_dict = {
'username': self.user_object.username,
'password1': 'test_Pass',
'password2': 'test_Pass',
'phone_number': self.user_object.phone_number,
'category': self.user_object.category,
}
# Make request
response = self.client.post(self.signup_url, signup_dict)
# Check status response
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(User.objects.count(), 2)
# Check database
new_user = User.objects.get(username=self.user_object.username)
self.assertEqual(
new_user.category,
self.user_object.category,
)
self.assertEqual(
new_user.phone_number,
self.user_object.phone_number,
)
def test_if_username_already_exists_dont_signup(self):
# Prepare data with already saved user
signup_dict = {
'username': self.user_saved.username,
'password1': 'test_Pass',
'password2': 'test_Pass',
'phone_number': self.user_saved.phone_number,
'category': self.user_saved.category,
}
# Make request
response = self.client.post(self.signup_url, signup_dict)
# Check status response
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
str(response.data['username'][0]),
'A user with that username already exists.',
)
# Check database
# Check that there is only one user with the saved username
username_query = User.objects.filter(username=self.user_saved.username)
self.assertEqual(username_query.count(), 1)
好吧,在这个文件中,我们有一个测试案例,定义为UserSignUpTestCase,里面有两个单元测试,一个是测试预期案例,另一个是测试错误案例。我想列出几个要点来讨论好的做法:
- 看一下单元测试的名称:你可以随心所欲地命名它们(除非你改变模式,否则总是以
test开始)。一个好的做法是做一个能描述它的名字。这将有助于你在测试和调试时,例如,如果一个测试失败,你想追踪错误。 - 在setUpClass方法中,我们调用
super()函数,记住它对Django很重要。此外,我们还设置了所需的实例,如API客户端、一个用户和一个faker实例。此外,我们用create函数将一个用户加载到数据库中。 - 注意,在setUpClass方法中定义的
sign_upURL,使用了Django的reverse。如果你愿意,你可以直接输入URL字符串。但是使用反向,你获得一个输入相关名称的URL。在dj-rest-auth包中,相关名称是rest_register。我建议你在你的项目中使用反向,以提高可维护性。当你定义URL时,给它们一个名字,然后在测试中使用反向。之后,如果一个URL改变了,你不需要在测试中改变任何东西,因为反向解决了这个问题。 - 为单元测试选择的一般结构是:
- 准备好数据。你可以使用用
build生成的实例来获取请求的数据。 - 提出请求。使用APIClient来提出请求。
- 检查响应。检查响应数据(如果有数据)和状态。在使用Django REST框架时,我推荐使用status。有了它,你就能以一种更简洁的方式获得HTTP状态码。如果你不想这样做,你可以直接写上数字。
- 检查数据库。如果请求在数据库中产生了变化,请证实一下,以确保该功能正确地修改了它。如果你有不修改数据库的案例,测试数据库保持不变是一个好的做法(就像第二个单元测试一样)。
- 准备好数据。你可以使用用
这是一个有两个单元测试的测试案例的例子。你可以添加更多的单元测试,例如,测试请求中的某些数据是否不正确或用户不能注册。此外,你可以在该文件(或其他文件)中添加更多的测试案例类,例如,测试应用程序的登录功能。
运行你的测试
一旦你编码了几个测试,你可以在项目的主文件夹(有manage.py文件的文件夹)中用这个命令执行它们。
python manage.py test这将搜索该文件夹下的所有测试文件,然后执行它们,显示每个单元测试的错误或成功。
自定义测试的执行
你可以自定义测试的执行,例如,你可以表示要测试:
- 只有一个Django应用程序,指定
<Django app name>。python manage.py test users
- 只有一个Django应用中的一个文件,指定为
<Django app name>.path.to.file。python manage.py test users.test.test_signup
- 只有一个测试案例类,指定:
<Django app name>.path.to.file.ClassName。python manage.py test users.test.test_signup.UserSignUpTestCase
- 只有一个单元测试函数,指定
<Django app name>.path.to.file.ClassName.function_name。python manage.py test users.test.test_signup.UserSignUpTestCase.test_if_data_is_correct_then_signup
你也可以在命令中添加一些标志来定制执行,例如。
--keepdb以避免在执行之间擦除数据库。这可以提高速度。--parallel并行运行测试,以提高在多核硬件上的速度。如果你这样做,请确保它们被很好地隔离开来。
总结
在这篇文章中,我介绍了一些使用Django和Django REST框架进行测试的基础知识。此外,我还展示了一些有用的工具和良好的实践,这些工具和实践更侧重于API的测试。我强调了一些例子,以及如何使用命名的框架运行和定制你的测试。我希望你喜欢阅读,我希望这个博客可以帮助你测试和改善你自己的Django API项目。