阅读(2736) (3)

Django Tutorial Part 10: Testing a Django web application

2017-05-15 17:26:34 更新
先决条件: 完成之前的所有教程主题,包括 Django教程第9部分:使用表单
目的: 了解如何为基于Django的网站编写单元测试。

概述

LocalLibrary 当前具有显示所有图书和作者列表的页面, Book 作者项目的详细视图,要更新 BookInstance 的页面以及创建,更新和删除 如果您在表单教程中完成了挑战,那么项目(和 )。 即使使用这个相对较小的网站,手动导航到每个网页并表面地检查一切是否按预期工作可能需要几分钟。 随着我们进行更改和增长网站,手动检查一切正常工作所需的时间只会增长。 如果我们像我们一样继续下去,最终我们将花大部分时间进行测试,并且很少有时间改进我们的代码。

自动化测试真的可以帮助解决这个问题! 明显的好处是,它们可以比手动测试运行得更快,可以测试更低的细节水平,并且每次都测试完全相同的功能(人类测试人员远没有那么可靠!)因为它们是快速的自动化测试 可以更经常地执行,并且如果测试失败,它们指向代码没有按预期执行的确切位置。

此外,自动化测试可以充当代码的第一个真实世界的"用户",迫使您严格定义和记录您的网站的行为方式。 通常它们是您的代码示例和文档的基础。 由于这些原因,一些软件开发过程从测试定义和实现开始,之后编写代码以匹配所需的行为(例如 Test-driven_development">测试驱动行为驱动的开发)。

本教程介绍了如何为Django编写自动测试,方法是在 LocalLibrary 网站中添加一些测试。

测试类型

有许多类型,级别和测试和测试方法的分类。 最重要的自动化测试是:

Unit tests
Verify functional behavior of individual components, often to class and function level.
Regression tests
Tests that reproduce historic bugs. Each test is initially run to verify that the bug has been fixed, and then re-run to ensure that it has not been reintroduced following later changes to the code.
Integration tests
Verify how groupings of components work when used together. Integration tests are aware of the required interactions between components, but not necessarily of the internal operations of each component. They may cover simple groupings of components through to the whole website.

注意:其他常见类型的测试包括黑盒,白盒,手动,自动,金丝雀,烟雾,一致性,验收,功能,系统,性能,负载和压力测试。 查找更多信息。

Django提供了什么用于测试?

测试网站是一项复杂的任务,因为它由几层逻辑组成 - 从HTTP级请求处理,查询模型到表单验证和处理,以及模板呈现。

Django提供了一个基于Python标准的小层次类的测试框架 unittest library.\">unittest"> unittest 库。尽管有这个名字,这个测试框架适用于单元测试和集成测试。 Django框架添加了API方法和工具来帮助测试web和Django特定的行为。这些允许您模拟请求,插入测试数据和检查应用程序的输出。 LiveServerTestCase) and tools for Django还提供了一个API( LiveServerTestCase )和工具, using different testing frameworks, for example you can\">a class ="external"href ="https://docs.djangoproject.com/en/1.10/topics/testing/advanced/#other-testing-frameworks">使用不同的测试框架,例如,您可以Selenium framework to simulate a user interacting with a live browser.\">与流行的 Selenium 框架集成,以模拟用户与实时浏览器进行交互。

unittest) test base classes (要写一个测试,你从任何Django(或 unittest )测试基类(SimpleTestCase\">topic / testing / tools /#simpletestcase"> SimpleTestCase TestCaseTransactionTestCase TestCase LiveServerTestCase) and then write separate methods to check that specific functionality works as expected (\">"external"href ="https://docs.djangoproject.com/en/1.10/topics/testing/tools/"> LiveServerTestCase ),然后编写单独的方法来检查特定功能是否按预期工作True or False values, or that two values are equal, etc.) When you start a test run, the framework\">测试使用"assert"方法来测试表达式是否导致 True False 值,或两个值相等等)。当您开始测试运行时,在您的派生类中执行所选的测试方法。测试方法独立运行,在类中定义的常见设置和/或拆除行为,如下所示。

class YourTestClass(TestCase):

    def setUp(self):
        #Setup run before every test method.
        pass

    def tearDown(self):
        #Clean up run after every test method.
        pass

    def test_something_that_will_pass(self):
        self.assertFalse(False)

    def test_something_that_will_fail(self):
        self.assertTrue(False)

大多数测试的最佳基本类型是 django.test.TestCase >。 此测试类在运行测试之前创建一个干净的数据库,并在其自己的事务中运行每个测试函数。 该类还拥有测试客户端 您可以使用模拟用户在视图级别与代码交互。 在下面的章节中,我们将集中在使用 > TestCase 基类。

你应该测试什么?

您应该测试自己的代码的所有方面,但不是作为Python或Django的一部分提供的任何库或功能。

例如,考虑下面定义的 Author 模型。 您不需要明确测试 first_name last_name 已正确存储为 CharField 在数据库中,因为这是由Django定义的 虽然当然在实践中,你将不可避免地在开发期间测试这个功能)。 也不需要测试 date_of_birth 已经验证为日期字段,因为这也是在Django中实现的。

但是,您应该检查用于标签的文本(名字,姓氏,出生日期,已过帐)以及分配给文本的字段大小( 100个字符 ),因为这些是你的设计的一部分,可以在将来打破/改变的东西。

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)
    
    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])
    
    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

同样,您应该检查自定义方法 get_absolute_url() :normal;"> __ str __()按需运行,因为它们是您的代码/业务逻辑。 在 get_absolute_url()的情况下,你可以相信Django reverse() 正确,所以你测试的是关联的视图实际上已经被定义。

请注意:精明的读者可能会注意到,我们还希望将出生日期和死亡日期限制为合理的价值,并检查死亡是否在出生后出现。 在Django中,这个约束将被添加到你的表单类中(虽然你可以为这些字段定义验证器,它们只能在表单级别使用,而不能在模型级别使用)。

考虑到这一点,我们开始考虑如何定义和运行测试。

测试结构概述

在我们进入"要测试什么"的详细信息之前,让我们先简单看一下 测试的定义。

Django使用unittest模块的内置测试发现, 它将在以 test * .py 模式命名的任何文件中的当前工作目录下发现测试。 如果您适当地命名文件,您可以使用任何您喜欢的结构。 我们建议您为测试代码创建一个模块,并为模型,视图,表单和您需要测试的任何其他类型的代码分别建立文件。 例如:

catalog/
  /tests/
    __init__.py
    test_models.py
    test_forms.py
    test_views.py

在您的 LocalLibrary 项目中创建如上所示的文件结构。 __ init __。py 应该是一个空文件(这告诉Python该目录是一个包)。 您可以通过复制和重命名骨架测试文件 /catalog/tests.py 来创建三个测试文件。

注意:当我们构建Django框架网站时,会自动创建骨架测试文件 /catalog/tests.py / a>。 将所有测试放在其中是完全"合法的",但是如果你正确测试,你会很快结束一个非常大和难以管理的测试文件。

删除骨架文件,因为我们不需要它。

打开 /catalog/tests/test_models.py 。 该文件已经导入 django.test.TestCase ,如下所示:

from django.test import TestCase

# Create your tests here.

通常,您将为要测试的每个模型/视图/表单添加一个测试类,并使用单个方法来测试特定功能。 在其他情况下,您可能希望有一个单独的类来测试特定的用例,使用单独的测试函数来测试该用例的各个方面(例如,一个类,用于测试模型字段是否经过正确验证, 每个可能的故障情况)。 再次,结构是非常取决于你,但最好是如果你是一致的。

将下面的测试类添加到文件的底部。 该类演示了如何通过从 TestCase 派生来构造测试用例类。

class YourTestClass(TestCase):

    @classmethod
    def setUpTestData(cls):
        print("setUpTestData: Run once to set up non-modified data for all class methods.")
        pass

    def setUp(self):
        print("setUp: Run once for every test method to setup clean data.")
        pass

    def test_false_is_false(self):
        print("Method: test_false_is_false.")
        self.assertFalse(False)

    def test_false_is_true(self):
        print("Method: test_false_is_true.")
        self.assertTrue(False)

    def test_one_plus_one_equals_two(self):
        print("Method: test_one_plus_one_equals_two.")
        self.assertEqual(1 + 1, 2)

新类定义了两种可用于预测试配置的方法(例如,创建测试所需的任何模型或其他对象):

  • setUpTestData() is called once at the beginning of the test run for class-level setup. You'd use this to create objects that aren't going to be modified or changed in any of the test methods.
  • setUp() is called before every test function to set up any objects that may be modified by the test (every test function will get a "fresh" version of these objects).

测试类也有一个 tearDown()方法,我们没有使用。 此方法对数据库测试不是特别有用,因为 TestCase 基类为您处理数据库拆卸。

下面我们有一些测试方法,使用 Assert 函数来测试条件是否为真,假或等于( AssertTrue AssertFalse AssertEqual )。 如果条件未按预期进行评估,则测试将失败,并将错误报告给控制台。

AssertTrue AssertFalse AssertEqual 是由 unittest 提供的标准断言。 框架中还有其他标准断言,还有 Django特定的断言 / a>来测试视图是否重定向( assertRedirects ),以测试特定模板是否已被使用( assertTemplateUsed )等。

您应该通常在测试中包括 print()功能,如上所示。 我们在这里只做,以便您可以看到在控制台中调用安装函数的顺序(在下一节)。

如何运行测试

运行所有测试的最简单的方法是使用命令:

python3 manage.py test

这将发现以当前目录下的 test * .py 模式命名的所有文件,并运行使用适当的基类定义的所有测试(这里我们有一些测试文件,但只有 / catalog /tests/test_models.py 目前包含任何测试。)默认情况下,测试将单独报告测试失败,然后是测试摘要。

LocalLibrary 的根目录中运行测试。 你应该看到一个类似下面的输出。

>python manage.py test

Creating test database for alias 'default'...
setUpTestData: Run once to set up non-modified data for all class methods.
setUp: Run once for every test method to setup clean data.
Method: test_false_is_false.
.setUp: Run once for every test method to setup clean data.
Method: test_false_is_true.
FsetUp: Run once for every test method to setup clean data.
Method: test_one_plus_one_equals_two.
.
======================================================================
FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\Github\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 3 tests in 0.075s

FAILED (failures=1)
Destroying test database for alias 'default'...

这里我们看到我们有一个测试失败,我们可以看到什么函数失败和原因(这个失败是期望的,因为 False 不是 True

提示:从上面的测试输出中学习的最重要的事情是,如果您为对象和方法使用描述性/信息性名称,它更有价值。

上述粗体中显示的文本通常不会显示在测试输出中(这是由测试中的 print()函数生成的)。 这显示了 setUpTestData()方法如何调用一次,而在每个方法之前调用 setUp()

接下来的部分显示了如何运行特定的测试,以及如何控制测试显示多少信息。

显示更多测试信息

如果您想获得有关测试运行的更多信息,可以更改详细信息。 例如,要列出测试成功以及失败(以及有关如何设置测试数据库的一大堆信息),您可以将verbosity设置为"2",如下所示:

python3 manage.py test --verbosity 2

允许的详细程度级别为0,1,2和3,默认值为"1"。

运行特定测试

如果要运行测试的一个子集,可以通过指定package(s),module, TestCase 子类或方法的完整的点路径来实现:

python3 manage.py test catalog.tests   # Run the specified module
python3 manage.py test catalog.tests.test_models  # Run the specified module
python3 manage.py test catalog.tests.test_models.YourTestClass # Run the specified class
python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two  # Run the specified method

LocalLibrary测试

现在我们知道如何运行我们的测试和什么样的东西我们需要测试,让我们看看一些实际的例子。

请注意:我们不会编写每一个可能的测试,但这应该给你和测试如何工作的想法,以及你能做什么。

楷模

如上所述,我们应该测试任何作为我们设计的一部分或由我们编写的代码定义的东西,而不是测试已经由Django或Python开发团队测试的库/代码。

例如,考虑下面的 Author 模型。 这里我们应该测试所有字段的标签,因为即使我们没有明确指定大多数字段,我们有一个设计,说明这些值应该是什么。 如果我们不测试值,那么我们不知道字段标签有其预期的值。 同样,当我们相信Django将创建一个指定长度的字段时,值得为此长度指定一个测试,以确保它按计划实现。

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)
    
    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])
    
    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

打开我们的 /catalog/tests/test_models.py ,然后使用作者模型的以下测试代码替换任何现有代码。

在这里,您将看到我们首先导入 TestCase ,并使用描述性名称从中导出我们的测试类( AuthorModelTest ),以便我们可以轻松识别测试中的任何失败的测试 输出。 然后我们调用 setUpTestData()创建一个作者对象,我们将在任何测试中使用但不修改。

from django.test import TestCase

# Create your tests here.

from catalog.models import Author

class AuthorModelTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        #Set up non-modified objects used by all test methods
        Author.objects.create(first_name='Big', last_name='Bob')

    def test_first_name_label(self):
        author=Author.objects.get(id=1)
        field_label = author._meta.get_field('first_name').verbose_name
        self.assertEquals(field_label,'first name')

    def test_date_of_death_label(self):
        author=Author.objects.get(id=1)
        field_label = author._meta.get_field('date_of_death').verbose_name
        self.assertEquals(field_label,'died')

    def test_first_name_max_length(self):
        author=Author.objects.get(id=1)
        max_length = author._meta.get_field('first_name').max_length
        self.assertEquals(max_length,100)

    def test_object_name_is_last_name_comma_first_name(self):
        author=Author.objects.get(id=1)
        expected_object_name = '%s, %s' % (author.last_name, author.first_name)
        self.assertEquals(expected_object_name,str(author))

    def test_get_absolute_url(self):
        author=Author.objects.get(id=1)
        #This will also fail if the urlconf is not defined.
        self.assertEquals(author.get_absolute_url(),'/catalog/author/1')

字段测试检查字段标签( verbose_name )的值以及字符字段的大小是否符合预期。 这些方法都有描述性名称,并遵循相同的模式:

author=Author.objects.get(id=1)   # Get an author object to test
field_label = author._meta.get_field('first_name').verbose_name   # Get the metadata for the required field and use it to query the required field data
self.assertEquals(field_label,'first name')  # Compare the value to the expected result

有趣的事情要注意:

  • We can't get the verbose_name directly using author.first_name.verbose_name, because author.first_name is a string (not a handle to the first_name object that we can use to access its properties). Instead we need to use the author's _meta attribute to get an instance of the field and use that to query for the additional information.
  • We chose to use assertEquals(field_label,'first name') rather than assertTrue(field_label == 'first name'). The reason for this is that if the test fails the output for the former tells you what the label actually was, which makes debugging the problem just a little easier.

注意:测试 last_name date_of_birth 标签,以及测试 last_name 字段的长度 被省略。 现在添加您自己的版本,遵循上面所示的命名约定和方法。

我们还需要测试我们的自定义方法。 这些基本上只是检查对象名称是否按照我们的预期使用"Surname,"Christian name"格式构造,并且我们获得的 Author 项目的URL是我们期望的。

def test_object_name_is_last_name_comma_first_name(self):
    author=Author.objects.get(id=1)
    expected_object_name = '%s, %s' % (author.last_name, author.first_name)
    self.assertEquals(expected_object_name,str(author))
        
def test_get_absolute_url(self):
    author=Author.objects.get(id=1)
    #This will also fail if the urlconf is not defined.
    self.assertEquals(author.get_absolute_url(),'/catalog/author/1')

立即运行测试。 如果您按照模型教程中的描述创建了作者模型,那么很可能您会得到 date_of_birth 标签的错误,如下所示。 测试失败,因为它是写期望标签定义遵循Django的惯例不大写标签的第一个字母(Django为您做这个)。

======================================================================
FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\...\locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label
    self.assertEquals(field_label,'died')
AssertionError: 'Died' != 'died'
- Died
? ^
+ died
? ^

这是一个非常小的错误,但它突出了写测试如何更彻底地检查您可能已经做出的任何假设。

请注意:将date_of_death字段(/catalog/models.py)的标签更改为"已死亡",然后重新运行测试。

用于测试其他模型的模式是类似的,因此我们不会继续进一步讨论这些模式。 随时为我们的其他模型创建自己的测试。

形式

测试你的表单的哲学和测试你的模型是一样的; 你需要测试任何你编码的或你的设计指定,但不是底层框架和其他第三方库的行为。

一般来说,这意味着您应该测试表单是否具有您想要的字段,并且这些字段显示有适当的标签和帮助文本。 您不需要验证Django是否正确验证字段类型(除非您创建了自己的自定义字段和验证),即您不需要测试电子邮件字段只接受电子邮件。 但是,您需要测试希望对字段执行的任何其他验证以及代码将为错误生成的任何消息。

考虑我们的图书更新形式。 这只有一个字段用于续订日期,该字段将有一个标签和帮助文本,我们需要验证。

class RenewBookForm(forms.Form):
    """
    Form for a librarian to renew books.
    """
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean_renewal_date(self):
        data = self.cleaned_data['renewal_date']

        #Check date is not in past.
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))
        #Check date is in range librarian allowed to change (+4 weeks)
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Remember to always return the cleaned data.
        return data

打开我们的 /catalog/tests/test_forms.py 文件,并用 RenewBookForm 表单的以下测试代码替换任何现有代码。 我们首先导入我们的表单和一些Python和Django库,以帮助测试与测试时间相关的功能。 然后,我们以与我们对模型相同的方式声明我们的形式测试类,为我们的 TestCase -derived测试类使用描述性名称。

from django.test import TestCase

# Create your tests here.

import datetime
from django.utils import timezone
from catalog.forms import RenewBookForm

class RenewBookFormTest(TestCase):

    def test_renew_form_date_field_label(self):
        form = RenewBookForm()        
        self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')

    def test_renew_form_date_field_help_text(self):
        form = RenewBookForm()
        self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).')

    def test_renew_form_date_in_past(self):
        date = datetime.date.today() - datetime.timedelta(days=1)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertFalse(form.is_valid())

    def test_renew_form_date_too_far_in_future(self):
        date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertFalse(form.is_valid())

    def test_renew_form_date_today(self):
        date = datetime.date.today()
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertTrue(form.is_valid())
        
    def test_renew_form_date_max(self):
        date = timezone.now() + datetime.timedelta(weeks=4)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertTrue(form.is_valid())

前两个函数测试字段的标签 help_text 符合预期。 我们必须使用字段字典访问字段(例如 form.fields [\'renewal_date\'] )。 注意这里我们还要测试标签值是否是 None ,因为即使Django会渲染正确的标签,如果值不是明确的,它返回 None 设置。

其余的函数测试表单是否在可接受范围内的续订日期有效,并且对范围之外的值无效。 请注意我们如何使用 datetime.timedelta()(在这种情况下指定天数或 周)。 然后我们创建表单,传入我们的数据,并测试它是否有效。

注意:这里我们实际上并不使用数据库或测试客户端。 请考虑修改这些测试,以使用 SimpleTestCase

我们还需要验证在表单无效时引发正确的错误,但这通常是作为视图处理的一部分进行的,所以我们将在下一节中介绍。

这是所有的形式; 我们有一些其他的,但它们是由我们的基于类的基于编辑的视图自动创建的,应该在那里测试! 运行测试并确认我们的代码仍然通过!

视图

要验证我们的观看行为,我们使用Django测试客户端 / a>。 这个类的行为就像一个虚拟的网络浏览器,我们可以使用它来模拟对网址的 GET POST 请求并观察响应。 我们可以看到几乎所有的响应,从低级HTTP(结果头和状态代码)到我们用来渲染HTML和我们传递给它的上下文数据的模板。 我们还可以看到重定向链(如果有的话),并在每个步骤检查URL和状态代码。 这允许我们验证每个视图是在做什么预期。

让我们从我们最简单的视图开始,它提供了所有作者的列表。 这会显示在网址 / catalog / authors / (网址配置中名为"authors"的网址)中。

class AuthorListView(generic.ListView):
    model = Author
    paginate_by = 10

因为这是一个通用的列表视图,几乎一切都由我们为Django做。 可以说,如果你信任Django,那么你唯一需要测试的是视图是可以访问的正确的URL,可以使用它的名称访问。 然而,如果你使用测试驱动开发过程,你将开始编写测试,确认视图显示所有作者,分页他们在10。

打开 /catalog/tests/test_views.py 文件,并使用 AuthorListView 的以下测试代码替换任何现有文本。 和以前一样,我们导入我们的模型和一些有用的类。 在 setUpTestData()方法中,我们设置了一些 Author 对象,以便我们可以测试我们的分页。

from django.test import TestCase

# Create your tests here.

from catalog.models import Author
from django.core.urlresolvers import reverse

class AuthorListViewTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        #Create 13 authors for pagination tests
        number_of_authors = 13
        for author_num in range(number_of_authors):
            Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,)
           
    def test_view_url_exists_at_desired_location(self): 
        resp = self.client.get('/catalog/authors/') 
        self.assertEqual(resp.status_code, 200)  
           
    def test_view_url_accessible_by_name(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)
        
    def test_view_uses_correct_template(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)

        self.assertTemplateUsed(resp, 'catalog/author_list.html')
        
    def test_pagination_is_ten(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)
        self.assertTrue('is_paginated' in resp.context)
        self.assertTrue(resp.context['is_paginated'] == True)
        self.assertTrue( len(resp.context['author_list']) == 10)

    def test_lists_all_authors(self):
        #Get second page and confirm it has (exactly) remaining 3 items
        resp = self.client.get(reverse('authors')+'?page=2')
        self.assertEqual(resp.status_code, 200)
        self.assertTrue('is_paginated' in resp.context)
        self.assertTrue(resp.context['is_paginated'] == True)
        self.assertTrue( len(resp.context['author_list']) == 3)

所有测试使用客户端(属于我们的 TestCase 的派生类)来模拟一个 GET 请求并获得响应( resp )。 第一个版本检查特定的URL(注意,只是没有域的特定路径),第二个版本从URL配置中的名称生成URL。

resp = self.client.get('/catalog/authors/')
resp = self.client.get(reverse('authors'))

一旦我们有响应,我们查询它的状态代码,使用的模板,响应是否分页,返回的项目数和项目总数。

我们上面演示的最有趣的变量是 resp.context ,它是视图传递给模板的上下文变量。 这对于测试非常有用,因为它允许我们确认我们的模板获得所需的所有数据。 换句话说,我们可以检查我们是否使用了预期的模板和模板获得的数据,这很大程度上要验证任何渲染问题都是由于模板。

Views that are restricted to logged in users

在某些情况下,您需要测试仅限登录用户的视图。 例如,我们的 LoanedBooksByUserListView 与我们以前的视图非常相似,但只适用于已登录的用户,并且只显示当前用户借用的 BookInstance 记录, 贷款"状态,并命令"最早的第一"。

from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
    """
    Generic class-based view listing books on loan to current user.
    """
    model = BookInstance
    template_name ='catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')

将以下测试代码添加到 /catalog/tests/test_views.py 。 这里我们首先使用 SetUp()创建一些用户登录帐户和 BookInstance 对象(以及它们相关的书和其他记录),我们稍后将在测试中使用它们。 一半的书籍是由每个测试用户借用的,但我们最初将所有书籍的状态设置为"维护"。 我们使用 SetUp()而不是 setUpTestData(),因为我们稍后将修改一些对象。

注意:以下 setUp()代码会创建指定语言的图书,但 Language 模型,因为它是作为一个挑战创建的。 如果是这种情况,只需注释掉创建或导入语言对象的代码部分。 您还应该在随后的 RenewBookInstancesViewTest 部分中执行此操作。

import datetime
from django.utils import timezone
        
from catalog.models import BookInstance, Book, Genre, Language
from django.contrib.auth.models import User #Required to assign User as a borrower

class LoanedBookInstancesByUserListViewTest(TestCase):

    def setUp(self):
        #Create two users
        test_user1 = User.objects.create_user(username='testuser1', password='12345') 
        test_user1.save()
        test_user2 = User.objects.create_user(username='testuser2', password='12345') 
        test_user2.save()
        
        #Create a book
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
        # Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre=genre_objects_for_book
        test_book.save()

        #Create 30 BookInstance objects
        number_of_book_copies = 30
        for book_copy in range(number_of_book_copies):
            return_date= timezone.now() + datetime.timedelta(days=book_copy%5)
            if book_copy % 2:
                the_borrower=test_user1
            else:
                the_borrower=test_user2
            status='m'
            BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status)
        
    def test_redirect_if_not_logged_in(self):
        resp = self.client.get(reverse('my-borrowed'))
        self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/')

    def test_logged_in_uses_correct_template(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))
        
        #Check our user is logged in
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)

        #Check we used correct template
        self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')

要验证如果用户未登录,视图将重定向到登录页面,请使用 assertRedirects ,如 test_redirect_if_not_logged_in()中所示。 要验证是否为登录用户显示该页面,我们首先登录我们的测试用户,然后再次访问该页面,并检查我们是否获得了一个 status_code 为200(成功)。

其余的测试验证我们的观点只返回贷款给我们当前的借款人的图书。 复制上面测试类末尾的(自解释)代码。

    def test_only_borrowed_books_in_list(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))
        
        #Check our user is logged in
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)
        
        #Check that initially we don't have any books in list (none on loan)
        self.assertTrue('bookinstance_list' in resp.context)
        self.assertEqual( len(resp.context['bookinstance_list']),0)
        
        #Now change all books to be on loan 
        get_ten_books = BookInstance.objects.all()[:10]

        for copy in get_ten_books:
            copy.status='o'
            copy.save()
        
        #Check that now we have borrowed books in the list
        resp = self.client.get(reverse('my-borrowed'))
        #Check our user is logged in
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)
        
        self.assertTrue('bookinstance_list' in resp.context)
        
        #Confirm all books belong to testuser1 and are on loan
        for bookitem in resp.context['bookinstance_list']:
            self.assertEqual(resp.context['user'], bookitem.borrower)
            self.assertEqual('o', bookitem.status)

    def test_pages_ordered_by_due_date(self):
    
        #Change all books to be on loan
        for copy in BookInstance.objects.all():
            copy.status='o'
            copy.save()
            
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))
        
        #Check our user is logged in
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)
                
        #Confirm that of the items, only 10 are displayed due to pagination.
        self.assertEqual( len(resp.context['bookinstance_list']),10)
        
        last_date=0
        for copy in resp.context['bookinstance_list']:
            if last_date==0:
                last_date=copy.due_back
            else:
                self.assertTrue(last_date <= copy.due_back)

你也可以添加分页测试,如果你愿意!

Testing views with forms

使用表单测试视图比上面的情况更复杂一些,因为您需要测试更多的代码路径:初始显示,数据验证失败后显示,验证成功后显示。 好消息是,我们使用客户端进行测试,几乎与我们对仅显示视图的方式一样。

为了演示,让我们为用于更新图书的视图编写一些测试(r enew_book_librarian()):

from .forms import RenewBookForm

@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
    """
    View function for renewing a specific BookInstance by librarian
    """
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_inst.due_back = form.cleaned_data['renewal_date']
            book_inst.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # If this is a GET (or any other method) create the default form
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,})

    return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})

我们需要测试该视图是否只对具有 can_mark_returned 权限的用户可用,并且如果用户尝试续订 BookInstance ,则会将用户重定向到HTTP 404错误页面, 代码>不存在。 我们应该检查表单的初始值是否为未来3周的日期播种,如果验证成功,我们将重定向到"全借书"视图。 作为检查验证失败测试的一部分,我们还将检查我们的表单是否发送相应的错误消息。

将测试类的第一部分(如下所示)添加到 /catalog/tests/test_views.py 的底部。 这将创建两个用户和两个图书实例,但只向一个用户授予访问视图所需的权限。 在测试期间授予权限的代码以粗体显示:

from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.

class RenewBookInstancesViewTest(TestCase):

    def setUp(self):
        #Create a user
        test_user1 = User.objects.create_user(username='testuser1', password='12345')
        test_user1.save()

        test_user2 = User.objects.create_user(username='testuser2', password='12345')
        test_user2.save()
        permission = Permission.objects.get(name='Set book as returned')
        test_user2.user_permissions.add(permission)
        test_user2.save()

        #Create a book
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
        # Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre=genre_objects_for_book
        test_book.save()

        #Create a BookInstance object for test_user1
        return_date= datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o')

        #Create a BookInstance object for test_user2
        return_date= datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')

将以下测试添加到测试类的底部。 这些检查只有具有正确权限的用户( testuser2 )才能访问视图。 我们检查所有情况:当用户没有登录时,当用户登录但没有正确的权限,当用户有权限但不是借款人(应该成功),以及当他们尝试 访问不存在的 BookInstance 。 我们还检查是否使用了正确的模板。

    def test_redirect_if_not_logged_in(self):
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
        self.assertEqual( resp.status_code,302)
        self.assertTrue( resp.url.startswith('/accounts/login/') )
        
    def test_redirect_if_logged_in_but_not_correct_permission(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        
        #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
        self.assertEqual( resp.status_code,302)
        self.assertTrue( resp.url.startswith('/accounts/login/') )

    def test_logged_in_with_permission_borrowed_book(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) )
        
        #Check that it lets us login - this is our book and we have the right permissions.
        self.assertEqual( resp.status_code,200)

    def test_logged_in_with_permission_another_users_borrowed_book(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        
        #Check that it lets us login. We're a librarian, so we can view any users book
        self.assertEqual( resp.status_code,200)

    def test_HTTP404_for_invalid_book_if_logged_in(self):
        import uuid 
        test_uid = uuid.uuid4() #unlikely UID to match our bookinstance!
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) )
        self.assertEqual( resp.status_code,404)
        
    def test_uses_correct_template(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        self.assertEqual( resp.status_code,200)

        #Check we used correct template
        self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')

添加下一个测试方法,如下所示。 这将检查表单的初始日期是三个星期。 注意我们如何能够访问表单字段的初始值的值(以粗体显示)。

    def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        self.assertEqual( resp.status_code,200)
        
        date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
        self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )

下一个测试(将它添加到类中)会检查如果更新成功,则视图重定向到所有借用图书的列表。 这里不同的是,我们第一次展示如何使用客户端 POST 数据。 后数据是post函数的第二个参数,并被指定为键/值的字典。

    def test_redirects_to_all_borrowed_book_list_on_success(self):
        login = self.client.login(username='testuser2', password='12345')
        valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} )
        self.assertRedirects(resp, reverse('all-borrowed') )

全部借用的视图已添加为挑战,您的代码可能会重定向到主页"/"。 如果是这样,修改测试代码的最后两行就像下面的代码。 请求中的 follow = True 可确保请求返回最终目标网址(因此检查 / catalog / 而不是 / )。

 resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future},follow=True )
 self.assertRedirects(resp, '/catalog/')

将最后两个函数复制到类中,如下所示。 这些会再次测试 POST 请求,但在这种情况下,请求的续订日期无效。 我们使用 assertFormError()来验证错误消息是否符合预期。

    def test_form_invalid_renewal_date_past(self):
        login = self.client.login(username='testuser2', password='12345')       
        date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} )
        self.assertEqual( resp.status_code,200)
        self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past')
        
    def test_form_invalid_renewal_date_future(self):
        login = self.client.login(username='testuser2', password='12345')
        invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} )
        self.assertEqual( resp.status_code,200)
        self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')

相同类型的技术可以用于测试另一个视图。

模板

Django提供测试API来检查视图是否调用了正确的模板,并允许您验证是否正在发送正确的信息。 但是没有特定的API支持在Django中测试您的HTML输出是否按预期呈现。

Django的测试框架可以帮助你编写有效的单元和集成测试 - 我们只是划破了底层单元测试框架的表面,更不用说Django的添加了(例如,查看如何使用 unittest.mock 修补第三方库,以便更彻底地 测试你自己的代码)。

虽然有许多其他测试工具,您可以使用,我们将突出两个:

  • Coverage: This Python tool reports on how much of your code is actually executed by your tests. It is particularly useful when you're getting started, and you are trying to work out exactly what you should test.
  • Selenium is a framework to automate testing in a real browser. It allows you to simulate a real user interacting with the site, and provides a great framework for system testing your site (the next step up from integration testing.

挑战自己

有更多的模型和意见,我们可以测试。 作为一个简单的任务,尝试为 AuthorCreate 视图创建一个测试用例。

class AuthorCreate(PermissionRequiredMixin, CreateView):
    model = Author
    fields = '__all__'
    initial={'date_of_death':'12/10/2016',}
    permission_required = 'catalog.can_mark_returned'

请记住,您需要检查您指定的或设计的一部分。 这将包括谁有访问权限,初始日期,使用的模板以及视图在成功时重定向的位置。

概要

编写测试代码既不是有趣也不迷人,因此通常留在最后(或根本不)在创建网站。 然而,它是确保您的代码在进行更改后可以安全释放并且具有成本效益的维护的重要部分。

在本教程中,我们向您展示了如何为模型,表单和视图编写和运行测试。 最重要的是,我们简要总结了你应该测试什么,这在开始使用时通常是最难的。 还有很多要知道的,但即使你已经学到了,你应该能够为您的网站创建有效的单元测试。

下一个和最后一个教程将展示如何部署您的精彩(和完全测试!)Dango网站。

也可以看看