天天看点

《Web接口开发与自动化测试基于Python语言》--第6章

《Web接口开发与自动化测试基于Python语言》–读书笔记

第6章 Django测试

这章来到本书的正题了。

Web应用的难点在于: HTTP层面的请求处理、表单验证和处理、模板渲染;

Django框架的测试模块解决的问题: 模拟请求、插入测试数据、检查应用输出。

6.1 unittest单元测试框架

6.1.1 单元测试框架

误区:

不用单元测试框架一样可以编写单元测试,单元测试本质上就是通过一段代码去测试另一段代码;

单元测试框架不仅可以用于程序单元级别的测试,同样可以用于UI自动化测试、接口自动化测试,以及移动APP自动化测试。

单元测试框架:

提供用例编写规范与执行: 单元测试框架提供了统一的用例编写规范,灵活指定不同级别的测试,如针对一个测试方法、一个测试类、一个测试文件,或者一个测试目录等不同级别的测试。

提供专业的比较方法: 测试用例最关键的步骤,实际测试结果与预期结果的比较,单元测试将这个比较过程命名为“断言”,单元测试框架提供了丰富的断言方法,eg:相等/不相等,包含/不包含,True/False等。

提供丰富的测试日志: 单元测试框架提供了丰富的执行日志,当测试用例执行失败的时候会抛出明确的失败信息,测试完成后提供结果信息,失败用例数、成功用例数、执行时间等。

单元测试框架可帮助我们完成不同级别测试的自动化:

  • 单元测试:unittest
  • HTTP接口自动化测试:unittest+Requests
  • Web UI自动化测试:unittest+Selenium
  • 移动自动化测试:unittest+Appium

6.1.2 编写单元测试用例

简单示例:

对两个整数的简单计算module.py:

<span style="color:#000000"><code class="language-python"><span style="color:#880000">#! /usr/bin python</span>
<span style="color:#880000"># -*- coding:utf-8 -*-</span>

<span style="color:#000088">class</span> <span style="color:#4f4f4f">Calculator</span><span style="color:#4f4f4f">()</span>:
    <span style="color:#009900">"""实现两个数的加、减、乘、除"""</span>

    <span style="color:#000088">def</span> <span style="color:#009900">__init__</span><span style="color:#4f4f4f">(self, a, b)</span>:
        self.a = int(a)
        self.b = int(b)

    <span style="color:#880000"># 加法</span>
    <span style="color:#000088">def</span> <span style="color:#009900">add</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#000088">return</span> self.a + self.b

    <span style="color:#880000"># 减法</span>
    <span style="color:#000088">def</span> <span style="color:#009900">sub</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#000088">return</span> self.a - self.b

    <span style="color:#880000"># 乘法</span>
    <span style="color:#000088">def</span> <span style="color:#009900">mul</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#000088">return</span> self.a * self.b

    <span style="color:#880000"># 除法</span>
    <span style="color:#000088">def</span> <span style="color:#009900">div</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#000088">return</span> self.a / self.b</code></span>
           
  • 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

编写对应的测试文件test.py:

<span style="color:#000000"><code class="language-python"><span style="color:#880000">#! /usr/bin python</span>
<span style="color:#880000"># -*- coding:utf-8 -*-</span>

<span style="color:#000088">import</span> unittest
<span style="color:#000088">from</span> module <span style="color:#000088">import</span> Calculator

<span style="color:#000088">class</span> <span style="color:#4f4f4f">ModuleTest</span><span style="color:#4f4f4f">(unittest.TestCase)</span>:

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:
        self.cal = Calculator(<span style="color:#006666">8</span>, <span style="color:#006666">4</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">tearDown</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#000088">pass</span>

    <span style="color:#000088">def</span> <span style="color:#009900">test_add</span><span style="color:#4f4f4f">(self)</span>:
        result = self.cal.add()
        self.assertEqual(result, <span style="color:#006666">12</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sub</span><span style="color:#4f4f4f">(self)</span>:
        result = self.cal.sub()
        self.assertEqual(result, <span style="color:#006666">4</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">test_mul</span><span style="color:#4f4f4f">(self)</span>:
        result = self.cal.mul()
        self.assertEqual(result, <span style="color:#006666">32</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">test_div</span><span style="color:#4f4f4f">(self)</span>:
        result = self.cal.div()
        self.assertEqual(result, <span style="color:#006666">2</span>)

<span style="color:#000088">if</span> __name__ == <span style="color:#009900">"__main__"</span>:
    <span style="color:#880000"># unittest.main()</span>
    <span style="color:#880000"># 构造测试集</span>
    suite = unittest.TestSuite()
    suite.addTest(ModuleTest(<span style="color:#009900">"test_add"</span>))
    suite.addTest(ModuleTest(<span style="color:#009900">"test_mul"</span>))
    suite.addTest(ModuleTest(<span style="color:#009900">"test_sub"</span>))
    suite.addTest(ModuleTest(<span style="color:#009900">"test_div"</span>))
    <span style="color:#880000"># 执行测试</span>
    runner = unittest.TextTestRunner()
    runner.run(suite)</code></span>
           
  • 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

通过unittest单元测试框架编写的测试用例,更加规范和整洁。

对代码进行解释:

  1. 首先,import导入unittest单元测试框架;
  2. 其次,创建ModuleTest类继承unittest.TestCase类;
  3. setUp()方法,用于测试用例执行前的初始化工作,eg:初始化变量、生成数据库测试数据、打开浏览器等;
  4. tearDown()方法,用于测试用例执行之后的善后工作,eg:清除数据库测试数据、关闭文件、关闭浏览器等;
  5. 然后,创建具体的测试用例,包含被测试数据、预期测试结果;
  6. 接下来,调用unittest.TestSuite()类的addTest()方法,向测试套件中添加测试用例,所谓测试套件可以理解为测试用例的集合;
  7. 最后,通过unittest.TextTestRunner()类的run()方法运行测试套件中的测试用例。

注意:

  1. 根据unittest单元测试框架的要求,测试用例必须以“test”开头,eg:test_add、test_mul;
  2. 如果想默认运行当前测试文件中的所有测试用例,可以使用:unittest.main()方法。

测试结果如下:

<span style="color:#000000"><code>[email protected]:~# python test.py 
<span style="color:#009900">....
----------------------------------------------------------------------</span>
Ran 4 tests in 0.001s

OK
[email protected]:~# </code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

执行结果中的“.”代表一条运行通过的用例。

6.2 Django测试

django.test.TestCase从unittest.TestCase继承而来。

6.2.1 一个简单的例子

Django创建应用时,默认创建了test.py文件,查看/guest/sign/tests.py。

针对模型编写测试用例:

<span style="color:#000000"><code class="language-python"><span style="color:#000088">from</span> django.test <span style="color:#000088">import</span> TestCase
<span style="color:#000088">from</span> sign.models <span style="color:#000088">import</span> Guest, Event

<span style="color:#880000"># Create your tests here.</span>
<span style="color:#000088">class</span> <span style="color:#4f4f4f">ModelTest</span><span style="color:#4f4f4f">(TestCase)</span>:

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:
        Event.objects.create(id=<span style="color:#006666">1</span>, name=<span style="color:#009900">"oneplus 3 event"</span>, status=<span style="color:#000088">True</span>, limit=<span style="color:#006666">2000</span>, address=<span style="color:#009900">"shenzhen"</span>, start_time=<span style="color:#009900">"2016-08-31 02:18:22"</span>)
        Guest.objects.create(id=<span style="color:#006666">1</span>, event_id=<span style="color:#006666">1</span>, realname=<span style="color:#009900">"alen"</span>, phone=<span style="color:#009900">'13711001101'</span>,email=<span style="color:#009900">"[email protected]"</span>, sign=<span style="color:#000088">False</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">test_event_models</span><span style="color:#4f4f4f">(self)</span>:
        result = Event.objects.get(name=<span style="color:#009900">"oneplus 3 event"</span>)
        self.assertEqual(result.address, <span style="color:#009900">"shenzhen"</span>)
        self.assertTrue(result.status)

    <span style="color:#000088">def</span> <span style="color:#009900">test_guest_models</span><span style="color:#4f4f4f">(self)</span>:
        result = Guest.objects.get(phone=<span style="color:#009900">"13711001101"</span>)
        self.assertEqual(result.realname, <span style="color:#009900">"alen"</span>)
        self.assertFalse(result.sign)</code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

对上述代码进行分析:

  1. 首先,还是创建ModelTest类继承django.test.TestCase测试类;
  2. 然后,setUp()方法,初始化针对发布会表和嘉宾表的测试数据;
  3. 最后,通过test_event_models()、test_guest_models()测试方法,分别查询创建的数据,并对返回结果进行断言是否符合预期;

注意:

千万不要单独执行tests.py文件,Django专门提供了test命令来运行测试,效果如下:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
/usr/local/lib/python2.7/dist-packages/pymysql/cursors.py:<span style="color:#006666">166</span>: Warning: (<span style="color:#006666">3135</span>, u<span style="color:#009900">"'NO_ZERO_DATE', 'NO_ZERO_IN_DATE' and 'ERROR_FOR_DIVISION_BY_ZERO' sql modes should be used with strict mode. They will be merged with strict mode in a future release."</span>)
  result = self._query(query)
/usr/local/lib/python2.7/dist-packages/pymysql/cursors.py:<span style="color:#006666">166</span>: Warning: (<span style="color:#006666">3090</span>, u<span style="color:#009900">"Changing sql mode 'NO_AUTO_CREATE_USER' is deprecated. It will be removed in a future release."</span>)
  result = self._query(query)
..
----------------------------------------------------------------------
Ran <span style="color:#006666">2</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>025s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Ps:这里的两个警告,也困扰了自己很久,本来是写了一个详细的排查和解决过程的,但是一不小心没保存,结果全没了,也懒得再重写了,这里就简单给大家介绍一下如何去掉这两个警告吧!

  • 第一个警告,需要修改Django的配置文件settings.py,将数据库的配置里设置SQL_MODES的地方注释掉:
<span style="color:#000000"><code>DATABASES = {
    <span style="color:#009900">'default'</span>: {
        <span style="color:#009900">'ENGINE'</span>: <span style="color:#009900">'django.db.backends.mysql'</span>,
        <span style="color:#009900">'HOST'</span>: <span style="color:#009900">'127.0.0.1'</span>,
        <span style="color:#009900">'PORT'</span>: <span style="color:#009900">'3306'</span>,
        <span style="color:#009900">'NAME'</span>: <span style="color:#009900">'guest'</span>,
        <span style="color:#009900">'USER'</span>: <span style="color:#009900">'root'</span>,
        <span style="color:#009900">'PASSWORD'</span>: <span style="color:#009900">'nsfocus'</span>,
        #<span style="color:#009900">'OPTIONS'</span>: {
        #    <span style="color:#009900">'init_command'</span>: <span style="color:#009900">"SET sql_mode='STRICT_TRANS_TABLES'"</span>,
        #},
    }
}</code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

然后修改MySQL数据库的配置文件:/etc/mysql/mysql.conf.d/mysqld.cnf,增加配置:

sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

  • 第二个警告就简单了,只需要将提示的内容直接从配置里去掉即可。

6.2.2 运行测试用例

test命令,提供了可以控制测试用例执行的级别。

运行sign应用下的所有测试用例:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test sign</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
..
----------------------------------------------------------------------
Ran <span style="color:#006666">2</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>041s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行sign应用下的tests.py测试文件:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test sign.tests</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
..
----------------------------------------------------------------------
Ran <span style="color:#006666">2</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>040s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行sign应用tests.py测试文件下的ModelTest测试类:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test sign.tests.ModelTest</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
..
----------------------------------------------------------------------
Ran <span style="color:#006666">2</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>027s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行sign应用tests.py测试文件下ModelTest测试类下面的test_event_models测试方法:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test sign.tests.ModelTest.test_event_models</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
.
----------------------------------------------------------------------
Ran <span style="color:#006666">1</span> test <span style="color:#000088">in</span> <span style="color:#006666">0.</span>058s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span> </code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用-p(或–pattern)参数模糊匹配测试文件:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test -p test*.py</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
..
----------------------------------------------------------------------
Ran <span style="color:#006666">2</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>029s

OK
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6.3 客户端测试

django.test.Client类,可以模拟一个虚拟的网络浏览器,可以测试视图views与Django的应用程序以编程方式交互:

  • 模拟“GET”和“POST”请求,观察响应结果,从HTTP(headers、status code)到页面内容;
  • 检查重定向链(如果有的话),再每一步检查URL和status code;
  • 用一个包括特定值得模板context来测试一个request被Django模板渲染。

示例:

<span style="color:#000000"><code>root<span style="color:#006666">@TEST:/home/test/guest# python manage.py shell</span>
Python <span style="color:#006666">2.7</span><span style="color:#006666">.12</span> (default, Nov <span style="color:#006666">19</span> <span style="color:#006666">2016</span>, <span style="color:#006666">06</span>:<span style="color:#006666">48</span>:<span style="color:#006666">10</span>) 
[GCC <span style="color:#006666">5.4</span><span style="color:#006666">.0</span> <span style="color:#006666">20160609</span>] on linux2
Type <span style="color:#009900">"help"</span>, <span style="color:#009900">"copyright"</span>, <span style="color:#009900">"credits"</span> <span style="color:#000088">or</span> <span style="color:#009900">"license"</span> <span style="color:#000088">for</span> more information.
(InteractiveConsole)
<span style="color:#006666">>>> </span><span style="color:#000088">from</span> django.test.utils <span style="color:#000088">import</span> setup_test_environment    <span style="color:#880000"># 导入setup_test_environment方法</span>
<span style="color:#006666">>>> </span>setup_test_environment()                                <span style="color:#880000"># 用于测试前初始化测试环境</span>
<span style="color:#006666">>>> </span><span style="color:#000088">from</span> django.test <span style="color:#000088">import</span> Client                          <span style="color:#880000"># 导入Client类</span>
<span style="color:#006666">>>> </span>c = Client()                                            
<span style="color:#006666">>>> </span>response = c.get(<span style="color:#009900">'/index/'</span>)                             <span style="color:#880000"># 通过get()请求/index/路径</span>
<span style="color:#006666">>>> </span>response.status_code                                    <span style="color:#880000"># 打印HTTP返回的状态码200代表请求成功</span>
<span style="color:#006666">200</span>
<span style="color:#006666">>>> </span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

6.3.1 测试首页

使用上面的方法对发布会首页进行测试,修改/guest/sign/tests.py:

<span style="color:#000000"><code><span style="color:#880000">#! /usr/bin python</span>
<span style="color:#880000"># -*- coding:utf-8 -*-</span>

<span style="color:#000088">from</span> django.test <span style="color:#000088">import</span> TestCase


<span style="color:#880000"># Create your tests here.</span>
<span style="color:#880000"># 测试sign应用的视图</span>
<span style="color:#000088">class</span> <span style="color:#4f4f4f">IndexPageTest</span><span style="color:#4f4f4f">(TestCase)</span>:

    <span style="color:#000088">def</span> <span style="color:#009900">test_index_page_renders_index_template</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试index视图'''</span>
        response = self.client.get(<span style="color:#009900">'/index/'</span>)             <span style="color:#880000"># 虽然没有导入django.test.Client类,但是self.client最终调用的依然是django.test.Client类的方法,请求/index/路径</span>
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)       <span style="color:#880000"># status_code获取HTTP返回的状态码,使用assertEqual断言状态码是否为200</span>
        self.assertTemplateUsed(response, <span style="color:#009900">'index.html'</span>)   <span style="color:#880000"># 使用assertTemplateUsed()断言服务器是否使用的是index.html模板进行响应</span></code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

6.3.2 测试登录动作

继续使用上面的方法对首页的登录动作进行测试,修改/guest/sign/tests.py:

<span style="color:#000000"><code><span style="color:#000088">class</span> <span style="color:#4f4f4f">LoginActionTest</span><span style="color:#4f4f4f">(TestCase)</span>:
    <span style="color:#009900">'''测试登录动作'''</span>

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:                                     <span style="color:#880000"># 初始化,调用User.objects.create_user创建登录用户数据</span>
        User.objects.create_user(<span style="color:#009900">'admin'</span>, <span style="color:#009900">'[email protected]'</span>, <span style="color:#009900">'admin123456'</span>)

    <span style="color:#000088">def</span> <span style="color:#009900">test_add_admin</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试添加的用户数据是否正确'''</span>
        user = User.objects.get(username=<span style="color:#009900">'admin'</span>)
        self.assertEqual(user.username, <span style="color:#009900">'admin'</span>)
        self.assertEqual(user.email, <span style="color:#009900">'[email protected]'</span>)    <span style="color:#880000"># 注意这里书中有误,user表里的字段是email而不是mail,否则会报错</span>

    <span style="color:#000088">def</span> <span style="color:#009900">test_login_action_username_password_null</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试用户名密码为空'''</span>
        test_data = {<span style="color:#009900">'username'</span>:<span style="color:#009900">''</span>, <span style="color:#009900">'password'</span>: <span style="color:#009900">''</span>}
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=test_data)    <span style="color:#880000"># 通过post()方法请求'/login_aciton/'路径测试登录功能</span>
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'username or password error!'</span>, response.content)   <span style="color:#880000"># assertIn()方法断言返回的HTML页面中是否包含指定的提示字符串</span>

    <span style="color:#000088">def</span> <span style="color:#009900">test_login_action_username_password_error</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试用户名密码错误'''</span>
        test_data = {<span style="color:#009900">'username'</span>:<span style="color:#009900">'abc'</span>, <span style="color:#009900">'password'</span>:<span style="color:#009900">'123'</span>}
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=test_data)
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'username or password error!'</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_login_action_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试登录成功'''</span>
        test_data = {<span style="color:#009900">'username'</span>:<span style="color:#009900">'admin'</span>, <span style="color:#009900">'password'</span>:<span style="color:#009900">'admin123456'</span>}
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=test_data)
        self.assertEqual(response.status_code, <span style="color:#006666">302</span>)    <span style="color:#880000"># 这里为什么断言的是302,是因为登录成功后,通过HttpResponseRedirect()跳转到了'/event_manage/'路径,这是一个重定向</span></code></span>
           
  • 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

6.3.3 测试发布会管理

继续使用上面的方法对发布会管理视图进行测试,修改/guest/sign/tests.py:

<span style="color:#000000"><code><span style="color:#000088">class</span> <span style="color:#4f4f4f">EventManageTest</span><span style="color:#4f4f4f">(TestCase)</span>:
    <span style="color:#009900">"""测试发布会管理"""</span>

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''初始化测试数据,包括登录用户数据,发布会数据'''</span>
        User.objects.create_user(<span style="color:#009900">'admin'</span>, <span style="color:#009900">'[email protected]'</span>, <span style="color:#009900">'admin123456'</span>)
        Event.objects.create(name=<span style="color:#009900">'xiaomi5'</span>, limit=<span style="color:#006666">2000</span>, address=<span style="color:#009900">'beijing'</span>, status=<span style="color:#006666">1</span>, start_time=<span style="color:#009900">'2017-08-10 12:30:00'</span>)
        self.login_user = {<span style="color:#009900">'username'</span>:<span style="color:#009900">'admin'</span>, <span style="color:#009900">'password'</span>:<span style="color:#009900">'admin123456'</span>}    <span style="color:#880000"># 定义登录变量</span>

    <span style="color:#000088">def</span> <span style="color:#009900">test_event_manage_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试发布会:xiaomi5'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/event_manage/'</span>)
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'xiaomi5'</span>, response.content)
        self.assertIn(<span style="color:#009900">b'beijing'</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_event_manage_search_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试发布会搜索'''</span>
        <span style="color:#880000"># 这里自己给自己挖了个坑,post登录请求的时候少写了一个/,当时写成了'/login_action',我擦一执行测试就返回302,排查了好半天才发现,哎,需要认真仔细啊</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/search_name/'</span>, {<span style="color:#009900">'name'</span>:<span style="color:#009900">'xiaomi5'</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'xiaomi5'</span>, response.content)
        self.assertIn(<span style="color:#009900">b'beijing'</span>, response.content)</code></span>
           
  • 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

注意:

由于发布会管理event_manage和发布会名称搜索search_name两个视图都被@login_required装饰器修饰,所以想测试这两个功能,必须要先登录成功,并且需要构造登录用户的数据。

6.3.4 测试嘉宾管理

继续使用上面的方法对嘉宾管理视图进行测试,修改/guest/sign/tests.py:

<span style="color:#000000"><code><span style="color:#000088">class</span> <span style="color:#4f4f4f">GuestManageTest</span><span style="color:#4f4f4f">(TestCase)</span>:
    <span style="color:#009900">"""测试嘉宾管理"""</span>

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''还是使用setUp初始化一些测试数据'''</span>
        User.objects.create_user(<span style="color:#009900">'admin'</span>, <span style="color:#009900">'[email protected]'</span>, <span style="color:#009900">'admin123456'</span>)
        Event.objects.create(id=<span style="color:#006666">1</span>, name=<span style="color:#009900">'xiaomi5'</span>, limit=<span style="color:#006666">2000</span>, address=<span style="color:#009900">'beijing'</span>, status=<span style="color:#006666">1</span>, start_time=<span style="color:#009900">'2017-08-10 12:30:00'</span>)
        Guest.objects.create(realname=<span style="color:#009900">'alen'</span>, phone=<span style="color:#006666">18611001100</span>, email=<span style="color:#009900">'[email protected]'</span>, sign=<span style="color:#006666">0</span>, event_id=<span style="color:#006666">1</span>)
        self.login_user = {<span style="color:#009900">'username'</span>:<span style="color:#009900">'admin'</span>, <span style="color:#009900">'password'</span>:<span style="color:#009900">'admin123456'</span>}

    <span style="color:#000088">def</span> <span style="color:#009900">test_event_manage_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试嘉宾信息:alen'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/guest_manage/'</span>)
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'alen'</span>, response.content)
        self.assertIn(<span style="color:#009900">b'18611001100'</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_guest_manage_search_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试嘉宾搜索功能'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        <span style="color:#880000"># 这里就是坑了,我们根据书中描述一步一步来得话,我们在views.py里定义的搜索功能是根据名字来搜索的,而不是根据手机号,下面应该修改为('/search_realname/', {'realname':'alen'})</span>
        <span style="color:#880000"># response = self.client.post('/search_phone/', {'phone':'18611001100'})</span>
        response = self.client.post(<span style="color:#009900">'/search_realname/'</span>, {<span style="color:#009900">'realname'</span>:<span style="color:#009900">'alen'</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b'alen'</span>, response.content)
        self.assertIn(<span style="color:#009900">b'18611001100'</span>, response.content)</code></span>
           
  • 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

上面的代码,虫师给大家挖了很多坑,如果只编写了测试代码而未进行实际测试,是不会发现有问题的,我已经备注了,大家参见上面的备注吧。

其他知识点没有什么,基本和上面的类似,都是setUp初始化测试数据,然后分别对两个视图函数进行测试。

6.3.5 测试用户签到

继续使用上面的方法对签到管理视图进行测试,修改/guest/sign/tests.py:

<span style="color:#000000"><code><span style="color:#000088">class</span> <span style="color:#4f4f4f">SignIndexActionTest</span><span style="color:#4f4f4f">(TestCase)</span>:
    <span style="color:#009900">"""测试发布会签到"""</span>

    <span style="color:#000088">def</span> <span style="color:#009900">setUp</span><span style="color:#4f4f4f">(self)</span>:
        User.objects.create_user(<span style="color:#009900">'admin'</span>, <span style="color:#009900">'[email protected]'</span>, <span style="color:#009900">'admin123456'</span>)
        Event.objects.create(id=<span style="color:#006666">1</span>, name=<span style="color:#009900">"xiaomi5"</span>, limit=<span style="color:#006666">2000</span>, address=<span style="color:#009900">'beijing'</span>, status=<span style="color:#006666">1</span>, start_time=<span style="color:#009900">'2017-8-10 12:30:00'</span>)
        Event.objects.create(id=<span style="color:#006666">2</span>, name=<span style="color:#009900">"oneplus4"</span>, limit=<span style="color:#006666">2000</span>, address=<span style="color:#009900">'shenzhen'</span>, status=<span style="color:#006666">1</span>, start_time=<span style="color:#009900">'2017-6-10 12:30:00'</span>)
        Guest.objects.create(realname=<span style="color:#009900">"alen"</span>, phone=<span style="color:#006666">18611001100</span>, email=<span style="color:#009900">'[email protected]'</span>, sign=<span style="color:#006666">0</span>, event_id=<span style="color:#006666">1</span>)
        Guest.objects.create(realname=<span style="color:#009900">"una"</span>, phone=<span style="color:#006666">18611011101</span>, email=<span style="color:#009900">'[email protected]'</span>, sign=<span style="color:#006666">1</span>, event_id=<span style="color:#006666">2</span>)
        self.login_user = {<span style="color:#009900">'username'</span>:<span style="color:#009900">'admin'</span>, <span style="color:#009900">'password'</span>:<span style="color:#009900">'admin123456'</span>}

    <span style="color:#000088">def</span> <span style="color:#009900">test_event_models</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试添加的发布会数据'''</span>
        result1 = Event.objects.get(name=<span style="color:#009900">'xiaomi5'</span>)
        self.assertEqual(result1.address, <span style="color:#009900">'beijing'</span>)
        self.assertTrue(result1.status)
        result2 = Event.objects.get(name=<span style="color:#009900">'oneplus4'</span>)
        self.assertEqual(result2.address, <span style="color:#009900">'shenzhen'</span>)
        self.assertTrue(result2.status)

    <span style="color:#000088">def</span> <span style="color:#009900">test_guest_models</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试添加的嘉宾数据'''</span>
        result = Guest.objects.get(realname=<span style="color:#009900">'alen'</span>)
        self.assertEqual(result.phone, <span style="color:#009900">'18611001100'</span>)
        self.assertEqual(result.event_id, <span style="color:#006666">1</span>)
        self.assertFalse(result.sign)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_phone_null</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试手机号为空'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/1/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">""</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"phone error."</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_phone_or_event_id_error</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试手机号或发布会id错误'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/2/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">"18611001100"</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"event id or phone error."</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_user_sign_has</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试嘉宾已签到'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/2/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">"18611011101"</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"user has sign in."</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_sign_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试嘉宾签到成功'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/1/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">"18611001100"</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"sign in success!"</span>, response.content)</code></span>
           
  • 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

测试嘉宾签到功能只是在数据初始化构造上内容多了,测试的点覆盖了签到功能全部分支,都比较好理解,只是在最终执行测试结果的时候,我崩溃了,6个测试用例中出现了2个失败,详细失败原因见下面:

<span style="color:#000000"><code>[email protected]:/home/test/guest<span style="color:#880000"># python manage.py test sign.tests.SignIndexActionTest</span>
Creating test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span>
..F18611001100
.F18611011101
.
======================================================================
FAIL: test_sign_index_action_phone_null (sign.tests.SignIndexActionTest)
测试手机号为空
----------------------------------------------------------------------
Traceback (most recent call last):
  File <span style="color:#009900">"/home/test/guest/sign/tests.py"</span>, line <span style="color:#006666">150</span>, <span style="color:#000088">in</span> test_sign_index_action_phone_null
    self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
AssertionError: <span style="color:#006666">404</span> != <span style="color:#006666">200</span>

======================================================================
FAIL: test_sign_index_action_sign_success (sign.tests.SignIndexActionTest)
测试嘉宾签到成功
----------------------------------------------------------------------
Traceback (most recent call last):
  File <span style="color:#009900">"/home/test/guest/sign/tests.py"</span>, line <span style="color:#006666">171</span>, <span style="color:#000088">in</span> test_sign_index_action_sign_success
    self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
AssertionError: <span style="color:#006666">404</span> != <span style="color:#006666">200</span>

----------------------------------------------------------------------
Ran <span style="color:#006666">6</span> tests <span style="color:#000088">in</span> <span style="color:#006666">0.</span>617s

FAILED (failures=<span style="color:#006666">2</span>)
Destroying test database <span style="color:#000088">for</span> alias <span style="color:#009900">'default'</span><span style="color:#000088">...</span></code></span>
           
  • 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

我了个去的,为什么完全照搬书中代码还会出现404错误,感觉会不会是虫师又在哪给我们挖坑了呢?

错误提示的很清晰,就是“测试手机号为空”、“测试嘉宾签到成功”这两个用例,在判断返回页面的status_code的时候出错了,预期是返回200,但实际返回了404。

WHY?怎么会在这两个地方出现404错误,查看测试代码:

<span style="color:#000000"><code>    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_phone_null</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试手机号为空'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/1/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">""</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"phone error."</span>, response.content)

    <span style="color:#000088">def</span> <span style="color:#009900">test_sign_index_action_sign_success</span><span style="color:#4f4f4f">(self)</span>:
        <span style="color:#009900">'''测试嘉宾签到成功'''</span>
        response = self.client.post(<span style="color:#009900">'/login_action/'</span>, data=self.login_user)
        response = self.client.post(<span style="color:#009900">'/sign_index_action/1/'</span>, {<span style="color:#009900">"phone"</span>:<span style="color:#009900">"18611001100"</span>})
        self.assertEqual(response.status_code, <span style="color:#006666">200</span>)
        self.assertIn(<span style="color:#009900">b"sign in success!"</span>, response.content)</code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

发现了一个共同点,这两个测试用例都是针对测试数据发布会id=1的,而与之关联的测试嘉宾的签到sign值在初始化的时候是sign=0,也就是未签到的状态。

将涉及到的两个值,分别做修改,如果我把这两个用例里的发布会id从1改为2,会发现再次执行的结果里“测试手机号为空”执行成功了,但是“测试嘉宾签到成功”依然还是失败的,捋一下,改成2,也就是对应的嘉宾已经签到了,所以“测试嘉宾签到成功”失败也是自然的。

如果我们把初始化数据里的嘉宾alen的sign改为1已签到,再次执行的时候,发现结果和上面一样,都是“测试手机号为空”能通过,但是“测试嘉宾签到成功”失败。

这里也真的是奇怪了,为什么会出现这种情况,我现在抛开测试数据,直接去看下真实的数据情况,因为之前为了验证嘉宾签到代码,已经全部都签到了,只能通过修改数据库的方式,将sign从1改为0,点击发布会页面的sign链接,发现的确会报404错误,奇怪了,为什么没嘉宾签到,就返回404错误呢?按理来说,即使没有嘉宾签到,从发布会点击签到页面,也应该展示签到页面,只不过显示的已签到数为0。

自己在这段时间里,跑偏了很久,想过是不是签到功能不完善,难道需要先将sign从初始化的0update为1,再执行测试用例?或者是提示404,找不到页面,那我就单独把sign_index_action的html从sign_index.html里独立出来?……

自己真的是跑偏了太久,休假前到休假后,中间隔了快一周时间,再次查看views.py里的签到功能代码才发现问题所在:

并不是虫师给大家挖坑,而是自己给自己挖了一个大坑!自己在做虫师的作业的时候,也就是在签到页面显示总的嘉宾数和已签到嘉宾数的时候,使用了一个擅自查资料使用的方法:get_list_or_404,一切的罪过都是由它而来,我们来看下面的代码就:

<span style="color:#000000"><code><span style="color:#880000"># 签到页面</span>
<span style="color:#006666">@login_required</span>
<span style="color:#000088">def</span> <span style="color:#009900">sign_index</span><span style="color:#4f4f4f">(request, eid)</span>:
    username = request.session.get(<span style="color:#009900">'user'</span>, <span style="color:#009900">''</span>)
    event = get_object_or_404(Event, id=eid)
    guest_list = len(get_list_or_404(Guest, event_id=eid))
    guest_sign = len(get_list_or_404(Guest, event_id=eid, sign=<span style="color:#006666">1</span>))
    <span style="color:#000088">return</span> render(request, <span style="color:#009900">'sign_index.html'</span>, {<span style="color:#009900">"user"</span>: username, <span style="color:#009900">"event"</span>: event, <span style="color:#009900">'guest_list'</span>: guest_list, <span style="color:#009900">'guest_sign'</span>: guest_sign})</code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里使用的get_list_or_404()方法,当sign=1的时候并没有问题,因为始终都能获取到已经签到的嘉宾数量,但是一旦sign=0,就会导致get_list_or_404方法直接返回404错误,而不是返回0个已签到嘉宾,自己当时以为查找到了一个类似get_object_or_404相类似的好方法去获取嘉宾数量,但是没想到当获取不到嘉宾数量的时候应该怎样展示!聪明反被聪明误啊!

那么该如何去修改呢?其实很简单,只要正常去查询数据库,获取到嘉宾总数和已签到嘉宾数量就可以了,修改后的代码如下:

<span style="color:#000000"><code><span style="color:#880000"># 签到页面</span>
<span style="color:#006666">@login_required</span>
<span style="color:#000088">def</span> <span style="color:#009900">sign_index</span><span style="color:#4f4f4f">(request, eid)</span>:
    username = request.session.get(<span style="color:#009900">'user'</span>, <span style="color:#009900">''</span>)
    event = get_object_or_404(Event, id=eid)
    guest_list = len(Guest.objects.filter(event_id=eid))
    guest_sign = len(Guest.objects.filter(event_id=eid, sign=<span style="color:#006666">1</span>))
    <span style="color:#000088">return</span> render(request, <span style="color:#009900">'sign_index.html'</span>, {<span style="color:#009900">"user"</span>: username, <span style="color:#009900">"event"</span>: event, <span style="color:#009900">'guest_list'</span>: guest_list, <span style="color:#009900">'guest_sign'</span>: guest_sign})</code></span>
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

修复后,再去执行测试用例,就全部通过了。

6.4 总结

至此,本章关于Django测试的内容就结束了,总结起来,就是如下几点:

  1. Django的test库,提供了丰富的单元测试方法;
  2. test库中的TestCase方法可以测试模型、视图;
  3. 每个测试用例必须以test命名开头;
  4. 一些断言关键字如:assertEqual判断是否相等、assertFalse判断为否、assertTrue判断为是、assertIn判断包含;
  5. 基本的测试套路就是构造数据,对指定测试内容传递数据进行测试,对返回结果进行判断。

更多关于Django测试方法技巧请参见官方文档:

Django测试部分官方文档

继续阅读