前言 ´・ᴗ・`
-
本节可以跳过 因为是测试方面的学习 其实也不用太深入
-
真正想弄 把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()
- non-modified 不会在测试中改变的常量用例 比如名字之类的
- 其他函数的设计 如def test_false_is_false(self):
- 设置断言assert 也类似“expected”
- AssertTrue 理想状态这个表达式是True 如果测试时为False 返回错误(raise exception)
- AssertFalse, AssertEqual 同理
- assertRedirects 测试是否成功重定向
- assertTemplateUsed 测试是否使用到了模板
- 第一批函数的设计:创建测试用例test Data
- 继承适合的测试类 比我我想用testcase
- 运行测试
- 运行所有测试
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应用
- 谢谢大佬支持! 萌新有礼了:)