Web Server

简单的Web Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import socket

EOL1 = '\n\n'
EOL2 = '\n\r\n'
body = '''Hello, world! <h1> from the5fire 《Django企业开发实战》</h1>'''
response_params = [
'HTTP/1.0 200 OK',
'Date: Sat, 10 jun 2017 01:01:01 GMT',
'Content-Type: text/plain; charset=utf-8',
'Content-Length: {}\r\n'.format(len(body)),
body,
]
response = b'\r\n'.join(response_params)

def handle_connection(conn, addr):
request = ""
while EOL1 not in request and EOL2 not in request:
request += conn.recv(1024)
print(request)
conn.send(response)
conn.close()

def main():
# socket.AF_INET 用于服务器与服务器之间的网络通信
# socket.SOCK_STREAM 基于TCP的流式socket通信
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口可复用,保证我们每次Ctrl C之后,快速再次重启
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('127.0.0.1', 8080))
# 可参考:https://stackoverflow.com/questions/2444459/python-sock-listen
serversocket.listen(1)
print('http://127.0.0.1:8080')

try:
while True:
conn, address = serversocket.accept()
handle_connection(conn, address)
finally:
serversocket.close()

if __name__ == '__main__':
main()

WSGI,全称是Web Server Gateway Interface(Web服务网关接口)。规定了Web Server如何跟应用程序进行交互。

1556286413602

表单Forms类

什么是表单?何时使用表单?
在web开发里表单的使用必不可少。表单用于让用户提交数据或上传文件,表单也用于让用户编辑已有数据。Django的表单Forms类的作用是把用户输入的数据转化成Python对象格式,便于后续操作(比如存储,修改)

自定义表单:
类似模型,Django表单也由各种字段组成。表单可以自定义,也可以由模型Models创建。值得注意的是模型里用的是verbose_name来描述一个字段, 而表单用的是label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django import forms
from .models import Contact


class ContactForm1(forms.Form):
   # 使用label
   name = forms.CharField(label="Your Name", max_length=255)
   email = forms.EmailField(label="Email address")


class ContactForm2(forms.ModelForm):
   
   class Meta:
       model = Contact
       fields = ('name', 'email',)

用户提交的数据可以通过以下方法与表单结合,生成与数据结合过的表单(Bound forms)。Django只能对Bound forms进行验证。

1
form = ContactForm(data=request.POST, files=request.FILES)

表单比较完整的运用:

1
2
3
4
5
6
7
8
9
# forms.py
from django import forms
from django.contrib.auth.models import User

class RegistrationForm(forms.Form):
   username = forms.CharField(label='Username', max_length=50)
   email = forms.EmailField(label='Email',)
   password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
   password2 = forms.CharField(label='Password Confirmation', widget=forms.PasswordInput)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# view.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.models import User
from .forms import RegistrationForm
from django.http import HttpResponseRedirect


def register(request):
   if request.method == 'POST':
       form = RegistrationForm(request.POST)
       if form.is_valid():
           username = form.cleaned_data['username']
           email = form.cleaned_data['email']
           password = form.cleaned_data['password2']
           # 使用内置User自带create_user方法创建用户,不需要使用save()
           user = User.objects.create_user(username=username, password=password, email=email)
           # 如果直接使用objects.create()方法后不需要使用save()
           return HttpResponseRedirect("/accounts/login/")

   else:
       form = RegistrationForm()
return render(request, 'users/registration.html', {'form': form})
1
2
3
4
{# 模板:registration.html #}
<form action=”.” method=”POST”>
{{ form.as_p }}
</form>

工作:

  1. 当用户通过POST方法提交表单,我们将提交的数据与RegistrationForm结合,然后验证表单RegistrationForm的数据是否有效。
  2. 如果表单数据有效,我们先用Django User模型自带的create_user方法创建user对象,再创建user_profile。用户通过一张表单提交数据,我们实际上分别存储在两张表里。
  3. 如果用户注册成功,我们通过HttpResponseRedirect方法转到登陆页面。
  4. 如果用户没有提交表单或不是通过POST方法提交表单,我们转到注册页面,生成一张空的RegistrationForm。

表单的验证

每个forms类可以通过clean方法自定义表单验证。如果你只想对某些字段进行验证,你可以通过clean_字段名方式自定义表单验证。如果用户提交的数据未通过验证,会返回ValidationError,并呈现给用户。如果用户提交的数据有效form.is_valid(),则会将数据存储在cleaned_data里。

通用视图里使用表单:
在Django基于类的视图(Class Based View)里使用表单也非常容易,只需定义form_class就好了。下面是一个使用CreateView创建一篇新文章的例子。

1
2
3
4
5
6
7
8
9
from django.views.generic.edit import CreateView
from .models import Article
from .forms import ArticleForm


class ArticleCreateView(CreateView):
   model = Article
   form_class = ArticleForm
   template_name = 'blog/article_create_form.html'

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# urls.py
url(r'^addtest$',views.addtest,name="addtest")

# forms.py
class AddForm(forms.Form):
a = forms.IntegerField()
b = forms.IntegerField()

# views.py
from .forms import AddForm
def addtest(request):
if request.method == 'POST':
form = AddForm(request.POST)
if form.is_valid():
a = form.cleaned_data['a']
b = form.cleaned_data['b']
return HttpResponse(str(int(a) + int(b)))
else:
form = AddForm()
return render(request, 'addtest.html', {'form': form})
1
2
3
4
5
6
{# addtest.html #}
<form method='post'>
{% csrf_token %}
{{ form }}
<input type="submit" value="提交">
</form>

1556670927798
1556670944607

如果打印一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def addtest(request):
if request.method == 'POST':
form = AddForm(request.POST)
if form.is_valid():
a = form.cleaned_data['a']
b = form.cleaned_data['b']
# 加一句print
print(form)
return HttpResponse(str(int(a) + int(b)))
else:
form = AddForm()
# 加一句print
print(form)
return render(request, 'addtest.html', {'form': form})

在第一次访问的时候得到:

1
2
<tr><th><label for="id_a">A:</label></th><td><input id="id_a" name="a" type="number" required /></td></tr>
<tr><th><label for="id_b">B:</label></th><td><input id="id_b" name="b" type="number" required /></td></tr>

在输入a=1.b=2提交得到:

1
2
<tr><th><label for="id_a">A:</label></th><td><input id="id_a" name="a" type="number" value="1" required /></td></tr>
<tr><th><label for="id_b">B:</label></th><td><input id="id_b" name="b" type="number" value="2" required /></td></tr>

所以总结一下,Django 的 forms 提供了:

  • 模板中表单的渲染(自动生成HTML表单元素)
  • 数据的验证工作,某一些输入不合法也不会丢失已经输入的数据。
  • 还可以定制更复杂的验证工作,如果提供了10个输入框,必须必须要输入其中两个以上,在 forms.py 中都很容易实现.

Form相关的对象包括:

  • Widget:用来渲染成HTML元素的工具,如:forms.Textarea对应HTML中的标签
  • Field:Form对象中的一个字段,如:EmailField表示email字段,如果这个字段不是有效的email格式,就会产生错误。
  • Form:一系列Field对象的集合,负责验证和显示HTML元素
  • Form Media:用来渲染表单的CSS和JavaScript资源。

如果我们要设置admin的字段名称,我们可以直接使用verbose_name:

1
name = models.CharField(max_length=128,verbose_name='姓名')

1556325130858

在Meta里也可以使用verbose_name:

1
2
class Meta:
verbose_name = verbose_name_plural = '学员信息'

以前我们要修改字段名的话,需要设置一个函数,现在我们只需要使用chioce参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# model.py
class Student(models.Model):
SEX_TIMES = [
(0,'未知'),
(1,'男'),
(2,'女'),
]
STATUS_TIMES = [
(0,'申请'),
(1,'通过'),
(2,'拒绝'),
]
status = models.IntegerField(choices=STATUS_TIMES,verbose_name='审核状态')
sex = models.IntegerField(choices=SEX_TIMES,verbose_name='性别')
1
2
3
4
5
6
7
8
9
10
{# templates/index.html#}
<table border="1" cellspacing="0" cellpadding="2">
{% for student in students_list %}
<tr>
{# 提供了get_XXX_display方法 #}
<td>{{ student.get_sex_display }}</td>
<td>{{ student.get_status_display }}</td>
</tr>
{% endfor %}
</table>

现在字段名显示的就不是0,1,2,而是男,女,未知,申请,通过,拒绝

网页的值传到服务器是通过<input><textarea>标签中的 name 属性来传递的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# urls.py
url(r'^$',views.index,name='index')

# views.py
def index(reqeust):
students = Student.objects.all()
if reqeust.method == 'POST':
form = StudentForm(reqeust.POST)
if form.is_valid():
# 使用form.save代替下面的手动创建Student对象
form.save()
# cleaned_data = form.cleaned_data
# student = Student()
# student.name = cleaned_data['name']
# student.sex = cleaned_data['sex']
# student.email = cleaned_data['email']
# student.profession = cleaned_data['profession']
# student.qq = cleaned_data['qq']
# student.status = cleaned_data['status']
# student.save()

# 在urls.py定义index的时候声明了name='index'
# 所以可以通过reverse来拿到name对应的url
return HttpResponseRedirect(reverse('index'))
else:
form = StudentForm()
context = {'students': students, 'form': form}
return render(reqeust, 'student/index.html', context=context)

# forms.py
class StudentForm(forms.ModelForm):
# 该方法会被form自动调用
def clean_qq(self):
qq = self.cleaned_data['qq']
if not qq.isdigit():
raise forms.ValidationError('必须是数字!')
return int(qq)

class Meta:
model = Student
fields = ('name','sex','profession','email','qq','phone','status')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{# index.html #}
<!DOCTYPE html>
<html>
<head>
<title>学员管理系统</title>
</head>
<body>
<h3><a href="/admin/">Admin</a></h3>
<ul>
{% for student in students %}
<li>{{ student.name }} - {{ student.get_status_display }}</li>
{% endfor %}
</ul>
<hr/>
<form action="/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit" />
</form>
</body>
</html>

cleaned_data对象是Django的form对用户提交的数据根据字段类型做完转换之后的结果。

CBV

前面的views.py有一个对于request.method的判断,我们可以使用一个类来封装:

1
2
3
4
5
6
7
8
9
10
11
12
# views.py
def index(reqeust):
students = Student.objects.all()
if reqeust.method == 'POST':
form = StudentForm(reqeust.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('index'))
else:
form = StudentForm()
context = {'students': students, 'form': form}
return render(reqeust, 'student/index.html', context=context)

改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# views.py
from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Student
from .forms import StudentForm,AddForm

from django.views import View

class IndexView(View):
template_name = 'index.html'

def get_context(self):
return {'students':Student.get_all()}

def get(self,reqeust):
context = self.get_context()
form = StudentForm()
context.update({'form':form})
return render(reqeust,self.template_name,context=context)

def post(self,reqeust):
form = StudentForm(reqeust.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('index'))
context = self.get_context()
context.update({'form':form})
return render(reqeust.self.template_name,context=context)

因为这是一个类,不再是一个方法.这时urls.py就需要改为:
这个 as_view 其实是对 get 和post 方法的包装。

1
2
3
4
# urls.py
from .views import IndexView

url(r'^$',IndexView.as_view(),name='index'),

简单来说:
一般请求的判断方法:

1
2
3
4
5
def view(request, *args, **kwargs):
if request.method.lower() == 'get':
do_something()
if request.method.lower() == 'post':
do_something()

现在使用使用View.as_view()代替判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClassName(View):
'''
继承View自动判断请求方法
'''
def post():
pass

def get():
pass

def other():
pass

#调用方法
url(url, ClassName.as_view(), name)

设计思想:
把视图函数的逻辑定义到类的方法里面去,然后在函数中实例化这个类,通过调用类的方法实现函数逻辑,而把逻辑定义在类中的一个好处就是可以通过继承复用这些方法。

1556693679889

使用分页功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# models.py
class Student(models.Model):
...
@classmethod
def get_all(cls):
return cls.objects.all()

# urls.py
url(r'^students/(\d+)/$',views.students,name='student_page')

# view.py
def students(reqeust, student_page):
paginator = Paginator(Student.get_all(),5)
students_list = paginator.page(student_page)
content = {'students_list': students_list}
return render(reqeust, 'student/students.html',context=content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{# students.html #}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>students</title>
<style type="text/css">
li{
display: inline;
}
</style>
</head>

<body>
<h1>students</h1>
<table border="1" cellspacing="0" cellpadding="2">
<tr>
<th>ID</th>
<th>姓名</th>
<th>性别</th>
<th>职业</th>
<th>EMAIL</th>
<th>QQ</th>
<th>电话</th>
<th>审核状态</th>
<th>创建时间</th>
</tr>

{% for student in students_list %}
<tr>
<td>{{student.pk}}</td>
<td>{{student.name}}</td>
<td>{{student.get_sex_display}}</td>
<td>{{student.profession}}</td>
<td>{{student.email}}</td>
<td>{{student.qq}}</td>
<td>{{student.phone}}</td>
<td>{{student.get_status_display}}</td>
<td>{{student.created_time}}</td>
</tr>
{% endfor %}
</table>

{% for idx in students_list.paginator.page_range %}
{% if idx == students_list.number %}
<li>
{{idx}}
</li>
{% else %}
<li>
<a href={% url 'student_sys:student_page' idx %}>{{idx}}</a>
</li>
{% endif %}
{% endfor %}
</body>
</html>

导入静态文件:

  • 在manage.py目录下创建static目录,该目录下创建css,js,img,json,other等等目录.不同目录存放不同的文件
  • setting.py文件添加STATICFILES_DIRS = [os.path.join(BASE_DIR,'static')]
    注意这里的'static'就是对应文件夹的名称.
  • 在 模板文件导入:<link rel="stylesheet" type="text/css" href="/static/student/css/student.css"/>

静态文件的反向解析:

  • {% load static from staticfiles %}
  • <link rel="stylesheet" type="text/css" href="{% static 'student/css/student.css' %}"/>

url的反向解析:

1
2
3
4
5
6
# project/urls.py
# 注意namespace是include的参数
url(r'^',include('student.urls',namespace='student_sys'))

# myapp/urls.py
url(r'^students/(\d+)/$',views.students,name='student_page'),
1
2
3
4
5
{# 导入 #}
<a href="{% url 'student_sys:student_page' 3 %}">3</a>
{# 或者 #}
{# 外面的引号可有可无 #}
<a href={% url 'student_sys:student_page' 3 %}>3</a>

使用中间件:

中间件位置示意图

方法:

  1. __init__.py:
    不需要传参数,服务器响应第一个请求的时候自动调用.用于确定是否启用该中间件.

  2. process_request(self,request):
    分配url匹配视图之前调用.一般情况下,我们可以在这里做一些校验,比如用户登录或者HTTP中是否有认证头之类的验证。
    返回None或者HttpResponse对象.

    ==如果返回 HttpResponse,那么接下来的处理方法只会执行 process_response==,其他方法将不会被执行。这里需要注意的是,如果你的 middleware是 settings配置的 MIDDLEWARE的第一个,那么剩下的middleware也不会被执行;==如果返回None,那么 Django会继续执行其他方法。==

  3. process_view(self,request,view_func,view_args,view_kwargs):
    调用视图之前执行,返回None或者HttpResponse对象.

    view_func就是我们将要执行的view方法。它的返回值跟 process_request一样,是 Httpresponse或者None,其逻辑也一样。如果返回None,那么 Django会帮你执行view函数,从而得到最终的 response

  4. process_template_response(self,request,response):
    在视图刚好执行完后调用,返回None或者HttpResponse对象.在这里可以使用render

    执行完上面的方法,并且 Django帮我们执行完view,拿到最终的 response后,如果使用了模板的 response(这是指通过 return render方式返回的 response),就会来到这个方法中。在这个方法中,我们可以对 response做一下操作,比如 Content-Type设置,或者其他 header的修改/增加。

  5. process_response(self,request,response):
    响应返回浏览器之前调用,返回HttpResponse对象.

    所有流程都处理完毕后,就来到了这个方法。这个方法的逻辑跟 process_template_response是完全一样的,只是后者是针对带有模板的 response的处理。

  6. process_exception(self,request,exception):
    当视图发生异常时调用,返回HttpResponse对象.

    在发生异常时,才会进入这个方法。哪个阶段发生的异常呢?可以简单理解为在将要调用的ⅵew中出现异常(就是==在 process_view的func函数中==)或者==返回的模板 response在渲染时发生的异常==。但是需要注意的是,如果你在 process_View中手动调用了func,就不会触发 process_exception了。这个方法接收到异常之后,可以选择处理异常,然后返回一个含有异常信息的 Httpresponse,或者直接返回None不处理,这种情况下 Django会使用自己的异常模板

多个中间件的调用顺序:

1556702234157

img

中间件处理顺序

Django框架的执行过程:(上面黄色箭头就是执行过程)

  1. request请求经过WSGI后,先进入中间件,开始先走process_request函数,然后走到由关系映射后,这里注意并没有直接进入视图函数,而是从头开始执行process_view()函数;然后再去执行与urls.py匹配的视图函数;
  2. 如果视图函数没有报错,那就直接挨个反过来从最后一个中间件开始,依次将返回的实例对象(也就是我们在视图函数中写的 return HttpResponse()等等)传递给每个中间件的process_response函数;最后再交给客户端浏览器;

如果执行视图函数出错,那就反过来从最后一个中间件开始,将错误信息传递给每个中间件的process_exception()函数,走完所有后,然后最终再走procss_response后,最终再交给客户端浏览器注意:视图函数的错误是由process_exception()函数处理的,从最后一个中间件开始,依次逐级提交捕捉到的异常然后最终交给procss_response()函数,将最终的错误信息交给客户端浏览器。

简单来说:

  • process_request,process_view,process_response都是返回None则继续运行,返回HttpResponse则开始执行process_response

  • process_template_response只有在返回值中有render才会被调用,必须返回一个继承有 render 方法的 response 对象

  • process_exception在视图函数发生错误时调用.process_View中手动调用了func,就不会触发 process_exception了.
    返回None则是不处理错误,错误将传递到下一个中间件的process_exception.返回HttpResponse则开始执行process_response

  • 多个中间件的执行顺序:
    处理视图前:正序
    处理视图后:倒序

    也就是说:
    process_request,process_view为正序执行,
    process_exception,process_response为倒序执行

img

img

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
from django.urls import reverse
from django.utils.deprecation import MiddlewareMixin

class TimeItMiddleware(MiddlewareMixin):
def process_request(self,request):
self.start_time = time.time()

# 统计调用view所消耗的时间
def process_view(self,request,func,*args,**kwargs):
if request.path != reverse('index'):
return None

start = time.time()
response = func(request)
costed = time.time() - start
print(f'process view: {costed:.2f}')
return response

def process_response(self,request,response):
costed = time.time() - self.start_time
return response

# setting.py
MIDDLEWARE = [
'middleware.student.middlewares.TimeItMiddleware',
]

单元测试

单元测试写在app里面的test.py.(与views.py同级)

如果使用MySQL数据库存储,那么Django就会自动生成一个测试用的数据库.
当然我们可以修改这个数据库的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangobook',
'USER':'root',
'PASSWORD':'5KVp2y7,k96o',
'HOST':'localhost',
'POST':'3306',
'TEST':{
# 修改测试用的数据库名称
'NAME':'mytestdatabase',
}
}
}

Django提供了一个名为 Test case的基类,我们可以通过继承这个类来实现自己的测试逻辑。在此之前,我们需要了解 Test case给我们提供了哪些方法。

  1. def setUp(self):用来初始化环境,包括创建初始化的数据,或者做一些其他准备工作
  2. def test_xxx(self):方法后面的_xxx可以是任意东西.以test开头的方法认为是需要测试的方法,跑测试时会被执行.每个需要被测试的方法是相互独立的.
  3. def tearDown(self):跟 setUp相对,用来清理测试环境和测试数据.

Model 层测试:
这一层的测试主要保证数据的写入和查询是可用的,同时也需要保证我们在 Model层所提供的方法是符合预期的。

比如,在 Model中增加了 sex_show这样的属性,用来展示sex这个字段的中文显示,而不是数字1或者2.当然,这个功能是 Django已经提供给我们的。

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.db import models

class Student(models.Model):
objects = StudentManager()
SEX_TIMES = [
(0,'未知'),
(1,'男'),
(2,'女'),
]
...
@property
def sex_show(self):
return dict(self.SEX_ITEMS)[self.sex]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# myapp/test.py
from django.test import TestCase

# Create your tests here.

from .models import Student

class StudentTestCase(TestCase):
def setUp(self):
Student.objects.create(
name='hyl',
sex=1,
email='xxxx.com',
profession='程序员',
qq=333
phone='22222'
)

def test_create_and_sex_show(self):
student = Student.objects.create(
name='YYYY',
sex=1,
email='zzzz.com',
profession='程序员',
qq=333
phone='22222'
)
self.assertEqual(Student.sex_show,'男','性别字段内容跟展示不一致')

def test_filter(self):
Student.objects.create(
name='hyl',
sex=1,
email='xxxx.com',
profession='程序员',
qq=333
phone='22222'
)
name = 'hyl'
students = Student.objects.filter(name=name)
self.assertEqual(students.count(),1,f'应该只存在一个名称为{name}的记录')

View 层测试
这一层更多的是功能上的测试,也是我们重点关注的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from django.test import TestCase,Client
from .models import Student

class StudentTestCase(TestCase):
def setUp(self):
Student.objects.create(
name='hyl',
sex=1,
email='xxxx.com',
profession='程序员',
qq=333
phone='22222'
)

def test_get_index(self):
# 测试首页的可用性
client = Client()
response = client.get('/')
self.assertEqual(response.status_code,200,'status code must be 200!')

def test_post_student(self):
client = Client()
data = dict(
name = 'test_for_post',
sex = 1,
email = '333@d.com',
profession = '程序员',
qq = '333',
phone = '3333'
)
response = client.post('/',data)
self.assertEqual(response.status_code,302,'status code must be 302')
response = client.get('/')
self.assertTrue(b'test_for_post' in response.content,
'response content must contain "test_for_post"')

执行单元测试:

1
python manage.py test