django应用的测试

徘徊边缘 提交于 2020-02-26 14:19:02

本文章默认用户使用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&#39;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())

有关.object.filter()

并在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学习第二弹,收工。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!