2015-01-14

Published on 2015-01-14


关于自动化测试

简而言之,自动化测试就是针对代码写好各种粒度的测试并令其能够自动进行,从而免去手工测试浪费的时间和精力,以及很可能的会漏测的点。那自动化测试会带来哪些好处呢?

  • 节省时间。虽然编写测试会消耗一点点时间,但省去了以后每次修改、重构之后都要进行的手动测试,大大节省了以后的时间。
  • 除了发现错误,还能预防错误。
  • 使得代码更有吸引力,毕竟,有测试的代码显得比没有测试的代码更可靠。
  • 对团队协作有一定帮助。

对测试来说,多即优美。在编写测试的时候,可能会发现测试增长得非常快,难以控制,甚至会远远多于正常的应用代码。然而,这是无所谓的,测试是越多越好的,但不需要去维护特别好的组织,甚至冗余测试也是无所谓的。写好之后他们就会默默地在那里发挥作用,你可以忘记他们。

如果你有强迫症的话,可以从以下方面稍稍组织一下测试。

  • 对每个模块或者视图,建立分离的测试类
  • 对每个需要测试的条件,建立分离的测试方法
  • 测试方法的名称与其测试的内容相关

关于Django中的测试

Django会自动生成一个test.py文件,但测试并不一定要写在这个文件中。Django会自动发现所有继承了TestCase的类并执行其中的测试方法。

另外,Django要求先执行makemigrations命令才能正常进行测试。

小甜点

假设正在做一个问卷调查polls,有一个Question类,表示目前为止会被发布的问题,那么显然只有近期发布的问题才会被填写,而过期的或者还未到发布日期的问题则不能被填写。针对这个特性,可以进行测试

# polls/test.py

import datetime
from django.utils import timezone
from django.test import TestCase

from polls.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.assertEqual(future_question.was_published_recently(), False)

    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.assertEqual(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.assertEqual(recent_question.was_published_recently(), True)

代码比较容易理解,不再多解释。运行测试的方法是

$ python manage.py test

在运行过程中,Django做了以下工作

  • 寻找测试用例
  • 找到django.test.TestCase的子类
  • 创建用于测试目的的一个数据库
  • 寻找测试方法,即以test开头的方法
  • 进行测试验证

测试View

Client

首先需要介绍一下Django的测试Client,也就是用来在View层次上模拟用户交互的一个辅助类。可以在django.test.Client或者django.test.TestCase.client中获取到它。如果需要获取某个URL,如/polls/的响应,可以使用

response = client.get(reverse('polls:index'))
response.status_code # 200, for example
response.content # '<h1>hello world</h1>', for example

其中reverse是用来避免将URL硬编码在测试中的,具体参数形式与URL的配置有关系。对应的URL配置形式和reverse参数如下:

# mysite/urls.py
url(r'^polls/', include('polls.urls')),

# polls/urls.py
url(r'^$', views.index),

# reverse
reverse('polls.views.index') # '/polls/'
from polls.views import index
reverse(index) # '/polls/'

或者

# mysite/urls.py
url(r'^polls/', include('polls.urls', namespace='polls')),

# polls/urls.py
url(r'^$', views.index, name='index'),

# reverse
reverse('polls:index') # '/polls/'

进行测试

测试代码如下,比较容易读懂,不再多做解释。

def create_question(question_text, days):
    """
    Creates a question with the given `question_text` 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.",
                            status_code=200)
        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.>']
        )

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)
        response = self.client.get(reverse('polls:detail',
                                   args=(future_question.id,)))
        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)
        response = self.client.get(reverse('polls:detail',
                                   args=(past_question.id,)))
        self.assertContains(response, past_question.question_text,
                            status_code=200)

进阶

进阶知识点包含以下内容,详情请阅读文档

  • RequestFactory: 与test Client基本一致,但它是用来产生一个request对象的,这样就可以直接使用这个request对象调用相应的views方法得到response,从而进行验证。
  • 多数据库支持
  • TransactionTestCase:优化Django自己的测试套装,不建议使用,可能会被废弃。
  • 测试可复用APP
  • 使用不同的测试框架
  • 与coverage.py协作:coverage.py是一个计算测试覆盖率的有用工具,Django可以很好地与其协作