尊重作者的劳动,转载请注明作者及原文地址 http://www.cnblogs.com/txwsqk/p/6515996.html
完全翻译自官方文档 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 datetime from django.utils import timezone from django.test import TestCase from .models import Question class 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.001s FAILED (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.001s OK Destroying 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_code 404 >>> # 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_code 200 >>> response.content b' <ul> <li><a href="/polls/1/">What's up?</a></li> </ul> ' >>> # If the following doesn't work, you probably omitted the call to >>> # setup_test_environment() described above >>> response.context['latest_question_list'] <QuerySet [<Question: What's 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