服务端编程(十五)- Django - 自动化测试入门

文章目录

    • 前言 ´・ᴗ・`
    • 基本概念
    • 测试框架问题
    • 测试范围 测试规则 —— 你想测试什么?
    • 测试过程一览
    • 测试Author 模型
    • 测试表单forms
    • 视图的测试
    • 总结 ´◡`

前言 ´・ᴗ・`

  • 本节可以跳过 因为是测试方面的学习 其实也不用太深入

  • 真正想弄 把selenium玩一下就好 这个框架更加直观 更贴近客户端

  • 经过前面的学习 我们已经把网站主体整完了 剩下的无非就是测试以及少许美化的问题
    虽然我们只是个很小的应用 但是页面也已经超过50张以上(根据你录入书的数量)
    那么手动的来debug 恐怕低效 无趣 而且容易漏掉
    这时 自动化测试派上用场

  • 本篇内容将会帮助你学习…

    • 1 自动化测试的基本概念
    • 2 如何利用Django进行自动化测试

基本概念

基本的几种测试方式是:

  • 单元测试Unit tests
    验证各个组件的功能行为,通常是类别和功能级别。
  • 回归测试regression tests
    测试重现历史错误。最初运行每个测试,以验证错误是否已修复,然后重新运行,以确保在以后更改代码之后,未重新引入该错误。
  • 集成测试 integration tests
    验证组件分组在一起使用时的工作方式。集成测试了解组件之间所需的交互,但不一定了解每个组件的内部操作。它们可能涵盖整个网站的简单组件分组。

测试框架问题

另外 对于测试的框架而言 Django 默认使用unitest
当然你可以和别的现有测试框架集成在一起玩 比如经典的selenium
对这个感兴趣的可以看我这篇文章简单入门:
python 应用(六)—— 爬虫(一)selenium 概述

谷歌还有别的测试框架 是基于js的 这超出目前讨论范围
当然期待我们学习NodeJS的时候 就可以玩玩puppeteer

测试范围 测试规则 —— 你想测试什么?

我们当然不用测试Django已经给我们封装好的应用啦
关键是我们写的代码
另外不可以想当然 比如 我在model规定 last_name max_length = 100
你可能觉得 这不是很清楚吗?为啥要测试?

  • 原因1:我们开发的时候 会频繁更改需求 会多个人协作
    改需求 我们就会把测试规则更改 也就测试规则是贴近需求的
    而代码就不好说了
    假设之前你写好的“100” 被别人改了呢?(团队协作问题)
    或者你前几天改了又忘了呢?
    如果有很多这种字段 我想问题还是很大的 即便你用git能看到commit
    还是不保险
  • 原因2 Django可能不会按你预期工作 毕竟有些东西不报错反而是最大的问题:)

总之 编写测试规则尽量 详尽

测试过程一览

首先说明 这里我们是运用Django内置的unitest库进行测试的
unitest提供了很多测试的套路 封装成类
如 SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase
我们实际测试我们具体的model等 就可以继承之 然后运用

  • 创建测试目录 确定测试类别
    我们是测试所有的 模型model/视图view/表单forms 因此需要分门别类 这里分成三类
    一般目录设计如下:
catalog/
  /tests/
    __init__.py
    test_models.py
    test_forms.py
    test_views.py

我们是另外创建tests文件来放测试规则文件的
test就是“测试工具包” 你看到__init__ 就知道了
注意 工具包内的文件 命名有规范的 不能随便写 按这上面来就行

  • 编写测试文件(也就是测试工具)
    • 继承适合的测试类 比我我想用testcase
      • from django.test import TestCase
    • 定义测试函数
      这个函数可能针对一个model的字段 或者表单的一个编辑框 随你
      • 第一批函数的设计:创建测试用例test Data
        • non-modified 不会在测试中改变的常量用例 比如名字之类的
          setUpTestData()
        • modified 会在测试中变化的变量用例 比如会更改的数字
          setUp()
      • 其他函数的设计 如def test_false_is_false(self):
      • 设置断言assert 也类似“expected”
        • AssertTrue 理想状态这个表达式是True 如果测试时为False 返回错误(raise exception)
        • AssertFalse, AssertEqual 同理
        • assertRedirects 测试是否成功重定向
        • assertTemplateUsed 测试是否使用到了模板
  • 运行测试
    • 运行所有测试 python manage.py test
    • 运行特定测试 如testmodel :python3 manage.py test catalog.tests.test_models
      注意 测试名称test.catalog.test_model 这种格式 写法
  • 观察测试结果
>python3 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'...

就两句话重点:

  • FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass)
    错在哪里:test_flase_is_true 这个函数上
  • AssertionError: False is not true
    怎么错的:理想状态是False 实际是True

不期待你能完全理解测试过程 但得有个大致印象

下面我们来实战:

测试Author 模型

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)

看看原来模型有什么限制(约束restrain)
然后 我们编写test_model 弄一个 AuthorModelTest 采用testCase:

from django.test import TestCase

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')

这里注意几点:

  • assertTrue 和 assertEqual的问题
    test_first_name_label(self): 也就是第二个函数末尾 我们其实有两种写法:
    assertEquals(field_label,‘first name’)
    assertTrue(field_label == ‘first name’)
    推荐采用第一种 因为反馈信息更多(告诉你first name实际值是多少)
  • 检查字段标签(所谓verbose_name)的值,以及字符字段的大小,是否符合预期。
    author=Author.objects.get(id=1) # 先拿到一个对象实例
    field_label = author._meta.get_field(‘first_name’).verbose_name # 拿到字段名
    self.assertEquals(field_label,‘first name’) #断言确认
    你可以照猫画虎 测试last_name等字段

测试表单forms

其实与测试模型差不多
我们图书馆应用主要还是续借日期这块需要测试 原来forms.py 代码:

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

针对于此 我们设计了下面的测试代码
包含:

  • label 表单显示的标签测试
  • help_text 表单显示的帮助信息
  • 输入日期的范围是否在未来 4周以内
    注意timedelta函数 datetime.timedelta(days=2) 指的是2天的时间差 weeks = 4 就是四周时间差

/catalog/tests/test_forms.py

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())

关注这里:我们传数据采用键值对的形式 而且这个“renewal_date” 是的确是来源于类属性的
然后我们调用类实例的is_valid() 来断言测试

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

视图的测试

这里我们拿author验证为例
实际上我们大多数工作给django做了 一般不需要验证太多
这里只是学习之用 举个栗子
/catalog/tests/test_views.py

from django.test import TestCase

# Create your tests here.

from catalog.models import Author
from django.urls 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)

这里 我们先验证他是否分页 (paginate)于是创造13个数据

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,)

然后后面再断言

        self.assertTrue('is_paginated' in resp.context)
        self.assertTrue(resp.context['is_paginated'] == True)
        self.assertTrue( len(resp.context['author_list']) == 10)

另外很重要的是测试url映射与reverse反向映射 我们用client.get来实现:)

    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)

总结 ´◡`

自动化测试又是一个大学问 个人是先不着急学这一块 最多用selenium来简单测试一波

  • 本文专栏
    MySQL专栏
  • 我的其他专栏 希望能够帮到你 ( •̀ ω •́ )✧
    • 手把手带你学后端(服务端)
    • python这么火 想要深入学习python 玩一下简单的应用嘛?
      python应用
  • 谢谢大佬支持! 萌新有礼了:)

你可能感兴趣的:(服务端编程)