首页 > 代码库 > django入门-测试-part5

django入门-测试-part5

完全翻译自官方文档 https://docs.djangoproject.com/en/1.10/intro/tutorial05/

前面好几段文字都是介绍什么是自动化测试,我们为什么需要写测试用例,还有现在流行的"测试驱动开发",总之有如下好处:

  • 节省你的时间
    你修改了工程,设计好多模块,如果没有自动化测试,好吧,手动一个个测吧
  • 测试不能发现问题,但是能阻止问题的发生
    没有测试用例,那么你程序的行为就是不可预见的,即使这是你写的代码,你实际也不知道它内部是怎么运行的;有了测试用例,当某一个地方出问题,它就能指出这部分有问题,即使你自己压根没有意识到这里有问题
  • 测试可以让你的代码更有吸引力
    当你写了一段很牛[A-Z][1]的代码,但是别人并不想看,因为你没有测试用例,那么别人就认为你的代码是不可信的
  • 测试让团队合作更愉快
    小的应用可能只有一个开发者,但是大部分复杂的应用是一个团队一起开发的,测试用例可以防止你的同事不小心干扰了你的代码

好了,总之就是为你的工程写测试 百利无一害 

在前面章节中我们是怎么验证代码的,通过django-admin的shell

>>> import datetime>>> from django.utils import timezone>>> from polls.models import Question>>> # create a Question instance with pub_date 30 days in the future>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))>>> # was it published recently?>>> future_question.was_published_recently()True

好了下面我们用django自带的测试功能--自动化测试

在你的应用目录下有一个 test.py  就是它

django的自动化测试系统会在你的应用目录下找test开头的文件然后运行它

我们往test.py里写个测试用例

import datetimefrom django.utils import timezonefrom django.test import TestCasefrom .models import Questionclass QuestionMethodTests(TestCase):    def test_was_published_recently_with_future_question(self):        """        was_published_recently() should return False for questions whose        pub_date is in the future.        """        time = timezone.now() + datetime.timedelta(days=30)        future_question = Question(pub_date=time)        self.assertIs(future_question.was_published_recently(), False)

运行一下

python manage.py test polls

运行这个命令都发生了什么呢

这个类继承了TestCase, 它会创建一个测试用的数据库,这个类的方法都应该是test开头的方法

运行结果如下

Creating test database for alias default...F======================================================================FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)----------------------------------------------------------------------Traceback (most recent call last):  File "/path/to/mysite/polls/tests.py", line 16, 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.001sFAILED (failures=1)Destroying test database for alias default...

显然测试失败了

我们创建了一个问题,这个问题的发布时间是30天后,那么我们显然希望was_published_recently()返回False

但是测试用例却返回了True,所以我们代码有bug,我们修复一下polls/models.py

def was_published_recently(self):    now = timezone.now()    return now - datetime.timedelta(days=1) <= self.pub_date <= now

在重新执行下 

python manage.py test polls

好了这回测试通过了

Creating test database for alias default....----------------------------------------------------------------------Ran 1 test in 0.001sOKDestroying test database for alias default...

现在我们修复了 was_published_recently()的bug,我们来加几个复杂的测试用例

def test_was_published_recently_with_old_question(self):    """    was_published_recently() should return False for questions whose    pub_date is older than 1 day.    """    time = timezone.now() - datetime.timedelta(days=30)    old_question = Question(pub_date=time)    self.assertIs(old_question.was_published_recently(), False)def test_was_published_recently_with_recent_question(self):    """    was_published_recently() should return True for questions whose    pub_date is within the last day.    """    time = timezone.now() - datetime.timedelta(hours=1)    recent_question = Question(pub_date=time)    self.assertIs(recent_question.was_published_recently(), True)

下面我们来测试视图函数

我们在创建question时,如果pub_date是未来的时间,那么按理说这不应该在页面显示的,就像京东明天12:00有抢购活动,那么在这个时间点之前是不应该让用户看到的

在继续讲之前,先介绍一个django的测试命令 django.test.Client, 它可以在视图层模拟用户的交互行为,我们可以用Client命令在test.py或shell中

>>> from django.test.utils import setup_test_environment>>> setup_test_environment()

这个命令会加载模板渲染功能,这样我们就可以检验一些额外的属性比如response.context

注意这个命令不会去创建测试数据库,它它真实的数据库数据做测试

还有不要忘了在settings.py里设置正确的时区TIME_ZONE

Client怎么用呢

>>> from django.test import Client>>> # create an instance of the client for our use>>> client = Client()
>>> # get a response from ‘/‘>>> response = client.get(/)>>> # we should expect a 404 from that address>>> response.status_code404>>> # on the other hand we should expect to find something at ‘/polls/‘>>> # we‘ll use ‘reverse()‘ rather than a hardcoded URL>>> from django.urls import reverse>>> response = client.get(reverse(polls:index))>>> response.status_code200>>> response.contentb\n <ul>\n \n <li><a href="http://www.mamicode.com/polls/1/">What&#39;s up?</a></li>\n \n </ul>\n\n>>> # If the following doesn‘t work, you probably omitted the call to>>> # setup_test_environment() described above>>> response.context[latest_question_list]<QuerySet [<Question: Whats up?>]>

好了回到正题,修复我们的index视图,之前的index视图是这样的,没有判断question的发布时间是否是小于现在的时间

class IndexView(generic.ListView):    template_name = polls/index.html    context_object_name = latest_question_list    def get_queryset(self):        """Return the last five published questions."""        return Question.objects.order_by(-pub_date)[:5]

修正这个问题,当pub_date小于现在的时间时不显示

def get_queryset(self):    """    Return the last five published questions (not including those set to be    published in the future).    """    return Question.objects.filter(        pub_date__lte=timezone.now()    ).order_by(-pub_date)[:5]

来测试一下这个新视图吧

def create_question(question_text, days):    """    Creates a question with the given `question_text` and published the    given number of `days` offset to now (negative for questions published    in the past, positive for questions that have yet to be published).    """    time = timezone.now() + datetime.timedelta(days=days)    return Question.objects.create(question_text=question_text, pub_date=time)class QuestionViewTests(TestCase):    def test_index_view_with_no_questions(self):        """        If no questions exist, an appropriate message should be displayed.        """        response = self.client.get(reverse(polls:index))        self.assertEqual(response.status_code, 200)        self.assertContains(response, "No polls are available.")        self.assertQuerysetEqual(response.context[latest_question_list], [])    def test_index_view_with_a_past_question(self):        """        Questions with a pub_date in the past should be displayed on the        index page.        """        create_question(question_text="Past question.", days=-30)        response = self.client.get(reverse(polls:index))        self.assertQuerysetEqual(            response.context[latest_question_list],            [<Question: Past question.>]        )    def test_index_view_with_a_future_question(self):        """        Questions with a pub_date in the future should not be displayed on        the index page.        """        create_question(question_text="Future question.", days=30)        response = self.client.get(reverse(polls:index))        self.assertContains(response, "No polls are available.")        self.assertQuerysetEqual(response.context[latest_question_list], [])    def test_index_view_with_future_question_and_past_question(self):        """        Even if both past and future questions exist, only past questions        should be displayed.        """        create_question(question_text="Past question.", days=-30)        create_question(question_text="Future question.", days=30)        response = self.client.get(reverse(polls:index))        self.assertQuerysetEqual(            response.context[latest_question_list],            [<Question: Past question.>]        )    def test_index_view_with_two_past_questions(self):        """        The questions index page may display multiple questions.        """        create_question(question_text="Past question 1.", days=-30)        create_question(question_text="Past question 2.", days=-5)        response = self.client.get(reverse(polls:index))        self.assertQuerysetEqual(            response.context[latest_question_list],            [<Question: Past question 2.>, <Question: Past question 1.>]        )

讲解:

create_question()函数让我们方便的创建question

上面的几个函数根据函数名就能知道是干什么的,不解释了

 

index视图修复完了,detail视图也得改

class DetailView(generic.DetailView):    ...    def get_queryset(self):        """        Excludes any questions that aren‘t published yet.        """        return Question.objects.filter(pub_date__lte=timezone.now())

一样的逻辑,当pub_date less than equeal 小于等于timezone.now()现在的时间则不显示

为detail写测试用例

class QuestionIndexDetailTests(TestCase):    def test_detail_view_with_a_future_question(self):        """        The detail view of a question with a pub_date in the future should        return a 404 not found.        """        future_question = create_question(question_text=Future question., days=5)        url = reverse(polls:detail, args=(future_question.id,))        response = self.client.get(url)        self.assertEqual(response.status_code, 404)    def test_detail_view_with_a_past_question(self):        """        The detail view of a question with a pub_date in the past should        display the question‘s text.        """        past_question = create_question(question_text=Past Question., days=-5)        url = reverse(polls:detail, args=(past_question.id,))        response = self.client.get(url)        self.assertContains(response, past_question.question_text)

继续总结

你会发现当你开始写测试用例,你的测试代码就疯狂的增加了,而且他们很多重复的内容,相比你写的代码,测试代码是那么的丑陋,这不要紧,因为大部分时间你都不会察觉到他们的存在,你可以继续你的开发任务,不过当你修改了视图逻辑,你也要同步更新你的测试用例,不然会有一大顿测试用例过不了

关于写测试用例,下面有几条好的建议:

  • 一个模型或视图一个单独的测试类
  • 每个测试的方法只测试一种情况
  • 测试方法的名字要顾名思义

扩展一下

我们现在只介绍了基本的测试功能,当你想测试浏览器的行为时可以使用 "Selenium"测试框架,这个框架不光能测试你的代码,还有你的javascript,django也包含了一个工具LiveServerTestCase来让你使用Selenium

当你的功能很负责时,你可能希望当你提交代码时能自动运行测试,那么你去关注一下"持续集成"相关的内容

现在很多ide可以检查你的代码覆盖率,用这个方法也能找出你的哪些代码还没有被测试到,这也能让你发现哪些代码已经没用了可以删掉了.

如果你发现有的代码无法被测试,很明显这段代码应该被重构或者删除.有个工具Coverage可以帮你识别无用的代码 参考https://docs.djangoproject.com/en/1.10/topics/testing/advanced/#topics-testing-code-coverage

django入门-测试-part5