Django学习23-测试


对于现代Web开发人员来说,自动化测试是一种非常有用的消除bug的方法。您可以使用一组测试 - 测试套件 - 来解决或避免许多问题:

  • 在编写新代码时,可以使用测试来验证代码是否按预期工作。
  • 当您重构或修改旧代码时,可以使用测试来确保您的更改不会意外地影响应用程序的运行。

测试Web应用程序是一项复杂的任务,因为Web应用程序由多层逻辑组成 :从HTTP级请求处理到表单验证和处理,再到模板渲染。使用Django的测试执行框架和各种实用程序,您可以模拟请求,插入测试数据,检查应用程序的输出,并通常验证您的代码正在执行它应该做的事情。
在Django中编写测试的首选方法是使用Python标准库中内置的unittest模块。您还可以使用任何其他Python测试框架; Django为这种集成提供了API和工具。

编写和运行测试

Django的单元测试使用Python标准库模块:unittest。 该模块使用基于类的方法定义测试。在tests.py文件中编写测试文件:

from django.test import TestCase
from learning_logs.models import Topic, Post, User
import time
# Create your tests here.


class TopicTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='TestUser', email='[email protected]', password='password')
        Topic.objects.create(owner=self.user, text='topic test 1')
        Topic.objects.create(owner=self.user, text='topic test 2')

    def test_topic_user(self):
        topic_1 = Topic.objects.get(text='topic test 1')
        topic_2 = Topic.objects.get(text='topic test 2')
        self.assertEqual(topic_1.owner, self.user)
        self.assertEqual(topic_2.owner, self.user)

Django在执行setUp()部分操作时,并不会真正向数据库表中插入数据。所以不用关心testDown()清理工作。
使用python manage.py test运行测试:

(venv) ulysses@ulysses:~/PycharmProjects/django_ulysses$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.364s

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

Django会使用内置的测试用例检测装置来检测基于unittest的测试用例。默认情况下,这将在当前工作目录下的任何名为test*.py的文件中发现测试。从输出结果看,当运行测试时会创建一个测试用的数据库,之后Django会运行这些测试。如果测试通过就会出现上面显示的ok信息,一旦测试出现问题就会提示详细的错误信息,如:

FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
    self.assertIs(future_poll.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

可以通过向./manage.py测试提供任意数量的“测试标签”来指定要运行的特定测试。每个测试标签都可以是包、模块、TestCase子类或测试方法的完整Python路径。例如:

# Run all the tests in the animals.tests module
$ ./manage.py test animals.tests

# Run all the tests found within the 'animals' package
$ ./manage.py test animals

# Run just one test case
$ ./manage.py test animals.tests.AnimalTestCase

# Run just one test method
$ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak

需要数据库支持的测试(即模型测试)时,不会使用真实的数据库,Django 会为测试创建单独的空白数据库。无论测试是通过还是失败,测试数据库都会在执行完所有测试后被销毁。可以使用test --keepdb选项阻止测试数据库被销毁。这将在运行之间保留测试数据库。如果数据库不存在,将首先创建它。还将应用任何迁移以使其保持最新。
在进行测试时,Django会无视DEBUG的设置,所有测试都会在DEBUG=False的环境下运行(除非设置--debug-mode)。每次测试后都不会清除缓存,如果在生产中运行测试,运行manage.py test fooapp可以将数据从测试插入到实时系统的缓存中。

test命令

django-admin test [test_label [test_label …]] ,运行所有应用下的单元测试

  • --failfast: 在测试失败后立即停止运行测试并报告失败;

  • --testrunner TESTRUNNER:控制用于执行测试的测试运行器类,使用TEST_RUNNER设置提供的值。

  • --noinput, --no-input:禁止所有用户提示。 典型的提示是关于删除现有测试数据库的警告。

  • --keepdb, -k:在测试运行之间保留测试数据库。 这样做的优点是可以跳过create和destroy操作,这可以大大减少运行测试的时间,特别是在大型测试套件中。 如果测试数据库不存在,它将在第一次运行时创建,然后为每次后续运行保留。 在运行测试套件之前,任何未应用的迁移也将应用于测试数据库。

  • --reverse, -r: 按相反的执行顺序对测试用例进行排序。 这可能有助于调试未正确隔离的测试的副作用。 使用此选项时,将保留按测试类分组。

  • --debug-mode:设置DEBUG=True, 这可能有助于解决测试失败问题;

  • --debug-sql, -d:为失败的测试启用SQL日志记录。 如果–verbosity为2,则还会输出传递测试中的查询;

  • --parallel [N]: 在单独的并行进程中运行测试。 由于现代处理器具有多个内核,因此可以更快地运行测试。默认情况下–parallel根据multiprocessing.cpu_count()为每个核心运行一个进程。 您可以通过将其作为选项的值来调整进程数,例如, --parallel=4,或者通过设置DJANGO_TEST_PROCESSES环境变量。

  • --tag TAGS
    仅运行标有指定标签的测试。 可以多次指定并与test --exclude-tag结合使用。

  • exclude-tag EXCLUDE_TAGS
    排除使用指定标记标记的测试。 可以多次指定并与test --tag结合使用。

测试工具

Django提供了一小组在编写测试时派上用场的工具。

The test client

测试客户端是一个Python类,用它来充当虚拟Web浏览器,可以以编程方式测试视图并与Django的应用程序进行交互。
测试客户端的功能:

  • 以URL的形式模拟POSTGET请求,从HTTP的首部状态码到页面的所有内容;
  • 查看重定向链(如果有)并检查每个步骤的URL和状态代码;
  • 使用包含特定值的模板上下文来渲染指定的Django模板;

总结来说就是,使用Django的测试客户端来确定正在呈现正确的模板,并且模板传递正确的上下文数据。一个简单的例子,在test_client.py中测试GET中文和英文的首页:

from django.test import TestCase, Client


class ClientTestCase(TestCase):
    def setUp(self):
        self.client = Client()

    def test_home_page_en(self):
        response = self.client.get('/en/')
        self.assertEqual(response.status_code, 200)
        self.assertTrue('Weicome to my site' in response.content.decode('utf-8'))

    def test_home_page_zh(self):
        response = self.client.get('/zh-hans/')
        self.assertEqual(response.status_code, 200)
        self.assertTrue('欢迎来到我的网站' in response.content.decode('utf-8'))

运行测试:

(venv) ulysses@ulysses:~/PycharmProjects/django_ulysses$ python manage.py test learning_logs.test_client
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.034s

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

测试用户登录,和创建新topic,使用POST请求:

    def setUp(self):
        User.objects.create_user('Admin', '[email protected]', 'admin12345')
        self.login = {'username': 'Admin', 'password': 'admin12345'}
        self.client = Client()

    def test_login(self):
        """测试登录页面"""
        response = self.client.post('/en/users/login/', **self.login)
        self.assertEqual(response.status_code, 200)
        # self.assertTrue(self.client.login(**self.login))

    def test_post_topic(self):
        """登录后,创建topic再读取"""
        self.client.login(**self.login)
        self.client.post('/en/new_topic/', {'text': '红尘多可笑,痴情最无聊'})
        topic = Topic.objects.get(text='红尘多可笑,痴情最无聊')
        self.assertIsNotNone(topic)
        response = self.client.get(f'/en/topic/{topic.id}/')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['topic'], topic)

有关test client可以参考官方文档:https://docs.djangoproject.com/en/2.1/topics/testing/tools/#the-test-client

LiveServerTestCase

LiveServerTestCase与TransactionTestCase基本相同,只有一个额外功能:它在安装时在后台启动一个实时Django服务器,并在拆卸时将其关闭。 这允许使用除Django虚拟客户端之外的自动测试客户端(例如Selenium客户端)在浏览器内执行一系列功能测试并模拟真实用户的操作。
实时服务器侦听localhost并绑定到端口0,端口0使用操作系统分配的空闲端口。 在测试期间,可以使用self.live_server_url访问服务器的URL。首先安装selenium:
pip install selenium
在test_liveserver.py中添加一个使用Selenium客户端的测试:

from django.test import LiveServerTestCase
from selenium.webdriver import Chrome, ChromeOptions
import re


class MySeleniumTests(LiveServerTestCase):
    """ 定位UI元素
        ID = "id"
        XPATH = "xpath"
        LINK_TEXT = "link text"
        PARTIAL_LINK_TEXT = "partial link text"
        NAME = "name"
        TAG_NAME = "tag name"
        CLASS_NAME = "class name"
        CSS_SELECTOR = "css selector"
    """
    # host = 'localhost'
    # port = 0

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        options = ChromeOptions()
        options.add_argument('--headless')
        cls.selenium = Chrome(options)
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_home_page(self):
        """
        测试打开主页
        cls.live_server_url: 'http://%s:%s' % (cls.host, cls.server_thread.port)
        """
        self.selenium.get(f"{self.live_server_url}/en/")
        self.assertTrue(re.search('Weicome\s+to\s+my\s+site', self.selenium.page_source))

使用--headless参数后,会打开一个无界面的浏览器。

测试登录,使用find_element_by_[] 来找寻网页上的可交互元素,使用click()模拟显示的鼠标的点击:


    def test_login(self):
        timeout = 5
        self.selenium.get(f"{self.live_server_url}/en/")
        self.selenium.find_element_by_link_text('Login').click()
        WebDriverWait(self.selenium, timeout).until(
            lambda driver: driver.find_element_by_name('username'))
        self.selenium.find_element_by_name('username').clear()
        self.selenium.find_element_by_name('username').send_keys('Admin')
        self.selenium.find_element_by_name('password').clear()
        self.selenium.find_element_by_name('password').send_keys('admin12345')
        self.selenium.find_element_by_name('submit').click()
        # time.sleep(2)
        element = WebDriverWait(self.selenium, timeout).until(
            lambda driver: driver.find_element_by_id('navbarDropdown'))
        self.assertTrue(re.search('Weicome\s+to\s+my\s+site', self.selenium.page_source))
        # self.selenium.find_element_by_id('navbarDropdown').click()
        element.click()
        # time.sleep(2)
        element = WebDriverWait(self.selenium, timeout).until(
            lambda driver: driver.find_element_by_name('profile'))
        element.click()
        # self.selenium.find_element_by_name('profile').click()
        self.assertTrue(re.search('<h2>Admin</h2>', self.selenium.page_source))

在切换页面时,使用WebDriverWait检查是否加载了新的一页。

测试覆盖率

码覆盖率描述了已测试的源代码量。 它显示了代码的哪些部分是通过测试运行的,哪些不是。它是测试应用程序的重要部分。Django可以轻松地coverage.py集成,后者是一种用于测量Python程序代码覆盖率的工具。 首先,安装 coverage: pip install coverage。接下来,从包含manage.py的项目文件夹中运行以下命令:coverage run --source='.' manage.py test myapp这将运行测试并收集项目中已执行文件的覆盖率数据。
可以通过键入以下命令来查看此数据的报告:coverage report.

(venv) ulysses@ulysses:~/PycharmProjects/django_ulysses$ coverage run --source='.' manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
Destroying test database for alias 'default'...
(venv) ulysses@ulysses:~/PycharmProjects/django_ulysses$ coverage report
Name                                                                                                          Stmts   Miss  Cover
---------------------------------------------------------------------------------------------------------------------------------
django_ulysses/__init__.py                                                                                        2      0   100%
django_ulysses/database_router.py                                                                                 0      0   100%
django_ulysses/settings.py                                                                                       62      0   100%
django_ulysses/sitemaps.py                                                                                       21      6    71%

使用coverage report -m查看miss的具体行号:

(venv) ulysses@ulysses:~/PycharmProjects/django_ulysses$ coverage report -m
Name                                                                                                          Stmts   Miss  Cover   Missing
-------------------------------------------------------------------------------------------------------------------------------------------
django_ulysses/__init__.py                                                                                        2      0   100%
django_ulysses/database_router.py                                                                                 0      0   100%
django_ulysses/settings.py                                                                                       62      0   100%
django_ulysses/sitemaps.py                                                                                       21      6    71%   13, 16, 21, 29, 32, 35
django_ulysses/urls.py                                                                                           11      0   100%
django_ulysses/wsgi.py                                                                                            8      8     0%   15-30
learning_logs/__init__.py                                                                                         0      0   100%

使用coverage html来生成完整的网页形式的报告:
在这里插入图片描述

测试用例的特征

夹具装载

如果数据库中没有任何数据,则基于数据库网站的测试用例没有多大用处。 在TestCase.setUpTestData()使用ORM方法添加数据,使测试更具可读性,也可以选择使用fixture添加数据。
一个fixture就是导入Django数据库的数据集合。假如在项目指定的fixture文件目录settings.FIXTURE_DIRS下已经有了数据,就可以指定一个fixtures类属性来使用它:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    fixtures = ['mammals.json', 'birds']

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

多数据库测试支持

在进行测试时,Django设置一个测试数据库,该数据库对应于settings中DATABASES定义中定义的每个数据库。 但是,运行Django TestCase所花费的大部分时间都被刷新调用所消耗,这确保了在每次测试运行开始时都有一个干净的数据库。 如果有多个数据库,则需要多次刷新(每个数据库一次)。这可能是一项耗时的活动 ,特别是如果测试不需要测试多数据库活动。
作为优化,Django仅在每次测试运行开始时刷新default默认数据库。 如果您的设置包含多个数据库,并且您的测试要求每个数据库都是干净的,则可以使用测试套件上的multi_db属性来请求完全刷新。

class TestMyViews(TestCase):
    multi_db = True

    def test_index_page_view(self):
        call_some_test_code()

在运行TestMyViews下的用例时,所有的测试数据库都会被刷新。multi_db标志还会影响TransactionTestCase.fixtures加载到哪些数据库。 默认情况下(当multi_db = False时),夹具仅加载到默认数据库中。 如果multi_db = True,则会将夹具加载到所有数据库中。

重载系统设置

使用SimpleTestCase.settings()临时更改某一设置并在运行测试代码后可以恢复为原始值。 针对这点,Django提供了一个标准的Python上下文管理器,可以像这样使用:

from django.test import TestCase

class LoginTestCase(TestCase):

    def test_login(self):

        # First check for the default behavior
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/')

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL='/other/login/'):
            response = self.client.get('/sekrit/')
            self.assertRedirects(response, '/other/login/?next=/sekrit/')

例子中在with语句模块中重写了LOGIN_URL,在这之外LOGIN_URL就会回复原值。
使用SimpleTestCase.modify_settings()可以修改列表内的系统设置:

from django.test import TestCase

class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        with self.modify_settings(MIDDLEWARE={
            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
            'remove': [
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
                'django.contrib.messages.middleware.MessageMiddleware',
            ],
        }):
            response = self.client.get('/')
            # ...

若是对于一整个测试方法或类都要重写设置,可以使用override_settings()modify_settings()装饰器:

rom django.test import TestCase, override_settings

@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):

    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

断言

python内置的测试模块unittest.TestCase提供了诸如assertTrueassertEqualassertIsNotNone之类的断言,Django的TestCase提供了除这些之外的断言。大多数这些断言方法给出的失败消息可以使用msg_prefix参数进行自定义。 该字符串将以断言生成的任何失败消息为前缀。 这使您可以提供其他详细信息,以帮助您确定测试套件中故障的位置和原因,可以参考Assertions

添加测试标签

使用@tag()装饰器可以给测试添加标签来选择要进行那些测试。

from django.test import tag

class SampleTestCase(TestCase):

    @tag('fast')
    def test_fast(self):
        ...

    @tag('slow')
    def test_slow(self):
        ...

    @tag('slow', 'core')
    def test_slow_but_core(self):
        ...
    @tag('slow', 'core')
	class SampleTestCase(TestCase):
    ...

在运行测试用例时,可以使用./manage.py test --tag=fast来选择。

邮件服务

如果您的任何Django视图使用Django的电子邮件功能发送电子邮件,您可能不希望每次使用该视图运行测试时都发送电子邮件。出于这个原因,Django的测试运行器会自动将所有Django发送的电子邮件重定向到虚拟发件箱。这使您可以测试发送电子邮件的各个方面 ,从发送到每封邮件内容到邮件数量 ,而无需实际发送邮件。
进行测试时,会使用django.core.mail.outbox来作为进行邮件接受方,而它也只会在使用locmem邮件后端时被调用。使用案例,检查django.core.mail.outbox的长度和内容:

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            'Subject here', 'Here is the message.',
            '[email protected]', ['[email protected]'],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

测试用的mail.outbox在每次开始运行TestCase时,都会被清空。可以使用mail.outbox = []手动清空。

管理命令测试

可以使用call_command()函数测试管理命令。输出可以重定向到StringIO:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

猜你喜欢

转载自blog.csdn.net/qq_19268039/article/details/84326127
今日推荐