django应用的测试
本文章默认用户使用win10系统,并且已经安装pycharm、git、django2.2.5及配套第三方库(python3.6.0及以上版本,且为anaconda环境)
前言
其实在上一期django文章中就有测试的出现,我们使用shell测试数据库的功能,但这属于手动测试
在这篇文章中,我们要介绍的是自动化测试,即当你创建好了一系列测试,每次修改应用代码后,就可以自动检查出修改后的代码是否还像你曾经预期的那样正常工作,而不需要花费大量时间来进行手动测试简直懒人福音
对于任何一个项目来说,编写自动化测试都是十分重要的
测试驱动
一般我们采取先写测试后写代码的原则
自动化测试
发现漏洞
我们在上篇文章的结尾说到,我们的投票系统存在一个bug: Question.was_published_recently() 方法其实并不能正常判断该问题是否在刚才成功创建,在这里我们可以手动测试
在进入虚拟环境与交互式命令台后,依次输入以下命令
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> future_question = Question(pub_date = timezone.now() + datetime.timedelta(days = 30))
>>> future_question.was_published_recently()
观察输出
我的输出:
True
很显然这是有问题的,未来才会创建的问题不可能是刚才创建的,这就说明整个项目存在bug
暴露漏洞
我们在外层’polls’文件夹中修改tests.py来完成自动化测试,代码如下:
1 |
from django.test import TestCase |
2 |
from django.utils import timezone |
3 |
from .models import Question |
4 |
|
5 |
|
6 |
import datetime |
7 |
|
8 |
|
9 |
class (TestCase): |
10 |
|
11 |
def test_was_published_recently_with_future_question(self): |
12 |
time = timezone.now() + datetime.timedelta(days=30) |
13 |
future_question = Question(pub_date=time) |
14 |
self.assertIs(future_question.was_published_recently(), False) |
在上述代码中,我们通过创建test_was_published_recently_with_future_question() 方法用于检查was_published_recently() 函数的返回值是否正确
我们在该方法中给出了一个未来的问题实例future_question,并告知tests.py文件其应该返回False
测试
在外层’mysite’文件夹中输入
python manage.py test polls
观察输出
我的输出:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:UsersAdministratorPycharmProjectsmy_djangopollstests.py", line 18, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (failures=1)
Destroying test database for alias 'default'...
在输出中会给出错误函数、错误位置、错误原因、错误样例等一系列信息,有利于编码人员快速定位bug所在
在这里,自动化测试流程为:
- python manage.py test polls 将会寻找 polls 应用里的测试代码
- 它找到了 django.test.TestCase 类
- 它创建一个特殊的数据库供测试使用
- 它在查找到的类中寻找测试方法——以 test 开头的方法
- 它找到 test_was_published_recently_with_future_question 方法,并创建了一个 pub_date 值为 30 天后的 Question 实例
- 接着使用 assertIs() 方法,发现 was_published_recently() 返回了 True,而我们期望它返回 False
修复漏洞
我们该如何修复这个bug呢?
我们首先知道,这个bug是因为函数无法识别问题的真实创建时间,将未来的问题默认为刚才创建的了
所以,针对这个问题,我们可以通过修改’polls’文件夹中的models.py文件里的方法,让出现bug的方法认为只有过去的问题才能返回True,代码如下:
1 |
import datetime |
2 |
|
3 |
from django.db import models |
4 |
from django.utils import timezone |
5 |
|
6 |
# Create your models here. |
7 |
class Question(models.Model): |
8 |
question_text = models.CharField(max_length=200) |
9 |
pub_date = models.DateTimeField('date published') |
10 |
def __str__(self): |
11 |
return self.question_text |
12 |
|
13 |
def was_published_recently(self): |
14 |
now = timezone.now() |
15 |
return now - datetime.timedelta(days=1) <= self.pub_date <= now |
16 |
|
17 |
|
18 |
class Choice(models.Model): |
19 |
question = models.ForeignKey(Question, on_delete=models.CASCADE) |
20 |
choice_text = models.CharField(max_length=200) |
21 |
votes = models.IntegerField(default=0) |
22 |
def __str__(self): |
23 |
return self.choice_text |
该代码中的was_published_recently()方法确保了在一天前到现在所创建的问题都返回True,其余返回False
我们可以重新做一次测试
观察输出
我的输出:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
这说明针对该方法的bug修复成功,这也同样意味着,该方法不会再出现bug,可以放心调用
更全面的测试
有这样一种情况:在修复一个 bug 时不小心引入另一个 bug
为了避免这种情况的出现虽然往往无法避免,我们需要进行更加全面的测试
我们通过修改’polls’文件夹中的tests.py文件来完善,代码如下:
1 |
from django.test import TestCase |
2 |
from django.utils import timezone |
3 |
from .models import Question |
4 |
|
5 |
|
6 |
import datetime |
7 |
|
8 |
|
9 |
class (TestCase): |
10 |
|
11 |
def test_was_published_recently_with_future_question(self): |
12 |
time = timezone.now() + datetime.timedelta(days=30) |
13 |
future_question = Question(pub_date=time) |
14 |
self.assertIs(future_question.was_published_recently(), False) |
15 |
|
16 |
def test_was_published_recently_with_old_question(self): |
17 |
time = timezone.now() - datetime.timedelta(days=1, seconds=1) |
18 |
old_question = Question(pub_date=time) |
19 |
self.assertIs(old_question.was_published_recently(), False) |
20 |
|
21 |
def test_was_published_recently_with_recent_question(self): |
22 |
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) |
23 |
recent_question = Question(pub_date=time) |
24 |
self.assertIs(recent_question.was_published_recently(), True) |
根据修改后的was_published_recently()方法,我们更新了我们的测试方法,使其符合修改后的该方法的所有的返回值(针对本方法,即未来与过去返回False,刚才返回True)
我们需要明白的是进行过测试的那些方法的行为永远是符合预期的
测试视图
修复了上述bug后,视图方面也会产生一定的问题:系统会发布所有问题,也包括那些未来的问题。如果将pub_date设置为未来某天,该问题应该在所填写的时间点才被发布,而在此之前是不可见的。
测试工具
Django 提供了一个供测试使用的 Client 来模拟用户和视图层代码的交互
我们通过交互式命令台来使用它,代码及其对应的输出如下:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> client = Client()
>>> response = client.get('/')
Not Found: /
>>> response.status_code
404
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'n <ul>n n <li><a href="/polls/1/">What's up?</a></li>n n </ul>n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
了解一定python爬虫requests库的同学应该能很快理解上述代码,我们可以将这里的client理解成requests
开始修复
我们通过修改’polls’文件夹中的views.py文件来修复视图,代码如下:
1 |
from django.shortcuts import get_object_or_404, render |
2 |
|
3 |
# Create your views here. |
4 |
from django.http import HttpResponseRedirect |
5 |
from django.views import generic |
6 |
from django.urls import reverse |
7 |
from django.utils import timezone |
8 |
|
9 |
from .models import Choice, Question |
10 |
|
11 |
class IndexView(generic.ListView): |
12 |
template_name = 'polls/index.html' |
13 |
context_object_name = 'latest_question_list' |
14 |
|
15 |
def get_queryset(self): |
16 |
return Question.objects.filter( pub_date__lte=timezone.now() ).order_by('-pub_date')[:5] |
17 |
|
18 |
|
19 |
class DetailView(generic.DetailView): |
20 |
model = Question |
21 |
template_name = 'polls/detail.html' |
22 |
|
23 |
|
24 |
class ResultsView(generic.DetailView): |
25 |
model = Question |
26 |
template_name = 'polls/results.html' |
27 |
|
28 |
|
29 |
def vote(request, question_id): |
30 |
question = get_object_or_404(Question, pk=question_id) |
31 |
try: |
大专栏 django应用的测试gutter">
32 |
selected_choice = question.choice_set.get(pk=request.POST['choice']) |
33 |
except (KeyError, Choice.DoesNotExist): |
34 |
return render(request, 'polls/detail.html', { |
35 |
'question': question, |
36 |
'error_message': "You didn't select a choice.", |
37 |
}) |
38 |
else: |
39 |
selected_choice.votes += 1 |
40 |
selected_choice.save() |
41 |
return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) |
在上述代码中,我们在get_queryset()方法中修改了原有的返回值,它由Question 的 pub_data 属性与 timezone.now() 相比较来判断是否应该显示此问题
编写测试
我们通过’polls’文件夹中的tests.py文件来编写测试,代码如下:
1 |
from django.test import TestCase |
2 |
from django.utils import timezone |
3 |
from django.urls import reverse |
4 |
from .models import Question |
5 |
|
6 |
|
7 |
import datetime |
8 |
|
9 |
|
10 |
class (TestCase): |
11 |
|
12 |
def test_was_published_recently_with_future_question(self): |
13 |
time = timezone.now() + datetime.timedelta(days=30) |
14 |
future_question = Question(pub_date=time) |
15 |
self.assertIs(future_question.was_published_recently(), False) |
16 |
|
17 |
def test_was_published_recently_with_old_question(self): |
18 |
time = timezone.now() - datetime.timedelta(days=1, seconds=1) |
19 |
old_question = Question(pub_date=time) |
20 |
self.assertIs(old_question.was_published_recently(), False) |
21 |
|
22 |
def test_was_published_recently_with_recent_question(self): |
23 |
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) |
24 |
recent_question = Question(pub_date=time) |
25 |
self.assertIs(recent_question.was_published_recently(), True) |
26 |
|
27 |
|
28 |
def create_question(question_text, days): |
29 |
time = timezone.now() + datetime.timedelta(days=days) |
30 |
return Question.objects.create(question_text=question_text, pub_date=time) |
31 |
|
32 |
|
33 |
class QuestionIndexViewTests(TestCase): |
34 |
def test_no_questions(self): |
35 |
response = self.client.get(reverse('polls:index')) |
36 |
self.assertEqual(response.status_code, 200) |
37 |
self.assertContains(response, "No polls are available.") |
38 |
self.assertQuerysetEqual(response.context['latest_question_list'], []) |
39 |
|
40 |
def test_past_question(self): |
41 |
create_question(question_text="Past question.", days=-30) |
42 |
response = self.client.get(reverse('polls:index')) |
43 |
self.assertQuerysetEqual( |
44 |
response.context['latest_question_list'], |
45 |
['<Question: Past question.>'] |
46 |
) |
47 |
|
48 |
def test_future_question(self): |
49 |
create_question(question_text="Future question.", days=30) |
50 |
response = self.client.get(reverse('polls:index')) |
51 |
self.assertContains(response, "No polls are available.") |
52 |
self.assertQuerysetEqual(response.context['latest_question_list'], []) |
53 |
|
54 |
def test_future_question_and_past_question(self): |
55 |
create_question(question_text="Past question.", days=-30) |
56 |
create_question(question_text="Future question.", days=30) |
57 |
response = self.client.get(reverse('polls:index')) |
58 |
self.assertQuerysetEqual( |
59 |
response.context['latest_question_list'], |
60 |
['<Question: Past question.>'] |
61 |
) |
62 |
|
63 |
def test_two_past_questions(self): |
64 |
create_question(question_text="Past question 1.", days=-30) |
65 |
create_question(question_text="Past question 2.", days=-5) |
66 |
response = self.client.get(reverse('polls:index')) |
67 |
self.assertQuerysetEqual( |
68 |
response.context['latest_question_list'], |
69 |
['<Question: Past question 2.>', '<Question: Past question 1.>'] |
70 |
) |
在上述代码中,create_question()方法封装了创建投票的流程,然后在QuestionIndexViewTests类中进行一系列的测试
本质上,测试就是假装一些管理员的输入,然后通过用户端的表现是否符合预期来判断新加入的改变是否破坏了原有的系统状态
优化测试
如果有人能通过规律猜测到未发布的问题的URL该怎么办?
我们通过对’polls’文件夹中的views.py文件中的DetailView类做一定的约束,代码如下:
1 |
class DetailView(generic.DetailView): |
2 |
model = Question |
3 |
template_name = 'polls/detail.html' |
4 |
|
5 |
def get_queryset(self): |
6 |
return Question.objects.filter(pub_date__lte=timezone.now()) |
并在tests.py文件中添加对应的测试,代码如下:
1 |
class QuestionDetailViewTests(TestCase): |
2 |
def test_future_question(self): |
3 |
future_question = create_question(question_text='Future question.', days=5) |
4 |
url = reverse('polls:detail', args=(future_question.id,)) |
5 |
response = self.client.get(url) |
6 |
self.assertEqual(response.status_code, 404) |
7 |
|
8 |
def test_past_question(self): |
9 |
past_question = create_question(question_text='Past Question.', days=-5) |
10 |
url = reverse('polls:detail', args=(past_question.id,)) |
11 |
response = self.client.get(url) |
12 |
self.assertContains(response, past_question.question_text) |
这些测试用来检验 pub_date 在过去的 Question 可以显示出来,而 pub_date 为未来的不可以显示
代码测试是一个较大的内容,一般我们对于每个模型和视图都建立单独的TestClass;每个测试方法只测试一个功能;给每个测试方法起个能描述其功能的名字
自定义
自定义页面
我们在外层’polls’文件夹中创建一个’static’的文件夹,django将会在此文件夹下查找页面对应的静态文件
我们在’static’文件夹下创建一个’polls’文件夹,在该文件夹中创建一个style.css文件,代码如下:
1 |
li a { |
2 |
color: green; |
3 |
} |
随后我们修改index.html,使其能够调用.css,代码如下:
1 |
{% load static %} |
2 |
|
3 |
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}"> |
4 |
|
5 |
{% if latest_question_list %} |
6 |
<ul> |
7 |
{% for question in latest_question_list %} |
8 |
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> |
9 |
{% endfor %} |
10 |
</ul> |
11 |
{% else %} |
12 |
<p>No polls are available.</p> |
13 |
{% endif %} |
如果要添加图片,请在内层’polls’文件夹中创建’images’文件夹,用来存放图片,这属于静态前端内容,再此不多做介绍
自定义后台
自定义后台过程较为繁琐,将在以后的文章中专门介绍,感兴趣的同学可以参考
结尾
如果已经看完这两篇文章,你应该对django整体框架有一定的了解,我们将在后面几篇文章中介绍更详细的django,比如管理文件、缓存、打包等
以上是django学习第二弹,收工。
django应用的测试的更多相关文章
- Django单元测试(二)------测试工具
The test client test client是一个python类,来模拟一个简单的“哑”浏览器,允许你来测试你的view函数.你可以使用test client完成下列事情: 1.模拟&quo ...
- Django权限管理测试
测试内容:当我单击登录页面登录的时候页面会弹出当前用户的个人信息 当我点击提交的时候可以看到我当前用户的所有权限: 测试成功,接下来看一下后台的简单代码: class User(models.Mode ...
- 关于Django启动创建测试库的问题
最近项目迁移到别的机器上进行开发,启动Django的时候,有如下提示: Creating test database for alias 'default' 其实这个可能是在Django启动按钮的设置 ...
- dapi 基于Django的轻量级测试平台一 设计思想
GitHub:https://github.com/yjlch1016/dapi 一.项目命名: dapi:即Django+API测试的缩写 二.设计思想: 模拟性能测试工具JMeter的思路, 实现 ...
- 统计 Django 项目的测试覆盖率
作者:HelloGitHub-追梦人物 文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试.现在 ...
- Django 模版语法 测试环境 ORM单表查询
模版语法 传值 视图函数向前端html页面传值,基本上所有的数据类型都可以渲染在前端页面上. views.py from django.shortcuts import render, redirec ...
- django 网站项目测试
视图和 URL 配置: 在先前创建的 meishiweb目录下的 meishiweb 目录新建一个 view.py 文件,并输入代码: 此时在浏览器即可访问: 证明已经成功 我们也可以修改成以下的规则 ...
- dapi 基于Django的轻量级测试平台七 怎样部署到生产环境
QQ群: GitHub:https://github.com/yjlch1016/dapi Nginx+uWSGI 前置条件:以下所有操作均在root账号下面进行如果不是root用户请注意权限问题因为 ...
- dapi 基于Django的轻量级测试平台八 Docker部署
QQ群: GitHub:https://github.com/yjlch1016/dapi 采用Docker+Supervisor+Nginx+uWSGI+Django 一.Dockerfile文件: ...
随机推荐
- PAT Basic 1013 数素数 (20) [数学问题-素数]
题目 令Pi表示第i个素数.现任给两个正整数M <= N <= 10^4,请输出PM到PN的所有素数. 输⼊格式: 输⼊在⼀⾏中给出M和N,其间以空格分隔. 输出格式: 输出从PM到PN的 ...
- JIT Debug Info 简介
原总结debug调试dump转储文件JITprocdumpJIT Debugging 前言 在上一篇介绍 JIT Debugging 的文章 -- 你需要了解的JIT Debugging 中,我们了解 ...
- Java类只加载一次的情况
一个类只加载一次: 调用Java命令. 创建对象时 访问静态成员时 Class.forName("包名.类名")
- Tomcat8 启动报错
Tomcat8启动报错: java.lang.NoSuchMethodError:javax.servlet.ServletContext.getClassLoader 在网上搜索后,发现此类问题大都 ...
- python获取当前时间戳
import time # 获取当前时间戳print(int(time.time()))
- 给Office文档添加水印效果【测试有效】
private void button1_Click(object sender, EventArgs e) { string test1 = "C:\\test.docx";// ...
- 关于mysql数据库连接异常处理
tomcat启动错误日志关键信息: 28-Aug-2019 14:22:55.014 SEVERE [localhost-startStop-1] org.apache.catalina.core.C ...
- linux epoll ET边沿触发
/***EPOLL ET 触发必须使用非阻塞,LT触发可以阻塞/非阻塞.*read 函数 非阻塞读需 忙轮寻 soket关闭返回0,循环读完数据*如果已经读完再读read返回 -1,errno=11( ...
- 通过javascri实现输入框只能输入数字
输入框只能输入数字 <input type="text" onkeyup="value=value.replace(/[^\d]/g,'');"> ...
- Zabbix常用监控项整理
zabbix常用key:http://blog.51cto.com/ttxsgoto/1771752 linux主机cpu使用率超过90%的时候报警:https://blog.csdn.net/reb ...