测试驱动开发(Django)1~6

准备工作:

1,安装Firefox

安装geckodriver

首先通过brew 安装

$ brew install geckodriver

然后设置配置文件~/.bash_profile文件

export PATH=$PATH:'/usr/local/Cellar/geckodriver/0.23.0/bin'

这里的路径一直到geckodriver下的bin目录

在安装完成后,终端会输出你的路径,在原来的路径后面加上bin就可以了。

最后执行代码让修改生效

source ~/.bash_profile

2,安装virtualenvwrapper

创建虚拟环境TDD:mkvirtualenv TDD

3,安装Django 1.11 和 selenium 3:

pip install 'django<1.12' 'selenium<4'

第1章  使用测试功能协助安装Django

1.1

from selenium import webdriver

browser = webdriver.Firefox()
browser.get('http://localhost:8000')

assert 'Django' in browser.title

运行结果

selenium.common.exceptions.WebDriverException: Message: Reached error page: about:neterror?e=connectionFailure&u=http%3A//localhost%3A8000/&c=UTF-8&f=regular&d=Firefox%20%E6%97%A0%E6%B3%95%E5%BB%BA%E7%AB%8B%E5%88%B0%20localhost%3A8000%20%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%BF%9E%E6%8E%A5%E3%80%82 

1.2 让Django运行起来

django-admin.py startproject TDDweb

 cd TDDweb

python manage.py runserver

1.3 创建Git仓库

在TDDweb目录中:git init .

数据库文件sqlite3和selenium日志geckodriver.log加入 .gitignore中忽略变动

echo 'db.sqlites' >> .gitignore

echo 'geckodriver' >> .gitignore

Git add .  添加当前文件夹中其他内容

git status   查看状态

发现pyc文件

git rm -r --cached TDDweb/__pycache__   删掉所有pyc

echo '__pycache__' >> .gitignore 

echo '*.pyc' >> .gitignore                   加入忽略文件

git status    发现忽略文件变化

Git add .gitignore  

git commit -m 'first commit'      提交

 

 

第2章  使用unittest 模块扩展功能测试

使用selenium驱动真实的浏览器进行测试,即从用户角度查看应用的运作,称之为功能测试。

功能测试=验收测试=端到端测试

用户故事:

from selenium import webdriver

browser = webdriver.Firefox()

#乔伊听说有一个很酷的在线待办事项应用
#她去看了这个应用的首页
browser.get('http://localhost:8000')
#她注意到网页的标题和头部包含'To-Do'这个词


assert 'To-Do' in browser.title

#应用邀请她输入一个待办事项

#她在文本中输入了'Buy peacock feathers'(购买孔雀羽毛)
#她的爱好是钓鱼

#她输入回车键后页面刷新了
#待办事项显示'1:Buy peacock feathers'

#页面又显示了一个文本框,可以输入其他待办事项
#她输入了'Use peacock feathers to make a fly'(做假蝇)
#她做事非常有条理

#页面再次刷新,她的清单显示两条事项

#她想知道这个网站是否会记住她的清单
#她看到网站为她生成了一个唯一的URL
#而且页面中有一个文字解说这个功能

#她访问那个URL,发现她的清单还在

#她很满意,去休息了

browser.quit()

2.2 使用unittest模块 

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author  : Marvin King
# Date     : 2019-01-31 
from selenium import webdriver
import unittest

class NewVisitorTest(unittest.TestCase):
    def setUp(self):       #测试执行前
        self.browser = webdriver.Firefox()

    def tearDown(self):    #测试方法执行后
        self.browser.quit()

    def test_can_start_a_list_and_retrieve_it_later(self):    #测试方法
        # 乔伊听说有一个很酷的在线待办事项应用
        # 她去看了这个应用的首页
        self.browser.get('http://localhost:8000')
        # 她注意到网页的标题和头部包含'To-Do'这个词
        self.assertIn('To-Do',self.browser.title)           #测试失败执行
        self.fail('Finish the test!')
        # 应用邀请她输入一个待办事项

        # 她在文本中输入了'Buy peacock feathers'(购买孔雀羽毛)
        # 她的爱好是钓鱼

        # 她输入回车键后页面刷新了
        # 待办事项显示'1:Buy peacock feathers'

        # 页面又显示了一个文本框,可以输入其他待办事项
        # 她输入了'Use peacock feathers to make a fly'(做假蝇)
        # 她做事非常有条理

        # 页面再次刷新,她的清单显示两条事项

        # 她想知道这个网站是否会记住她的清单
        # 她看到网站为她生成了一个唯一的URL
        # 而且页面中有一个文字解说这个功能

        # 她访问那个URL,发现她的清单还在

        # 她很满意,去休息了

if __name__ == '__main__':
    unittest.main(warnings='ignore')    #禁止抛出ResourceWarning

 AssertionError: 'To-Do' not found in 'Welcome to Django'

2.3 提交

 

git status查看状态发现只有functional_tests.py产生变化

git diff :查看与上次提交的变化

Git commit -a :自动添加已追踪文件(没有增加新文件)。

 

第3章 使用单元测试测试简单的首页

3.1 第一个Django应用,第一个单元测试

Django鼓励以应用的方式组织带了吗。

创建应用--待办事项清单:

python manage.py startapp lists

3.2 单元测试与功能测试的区别

功能测试是站在用户角度从外部测试应用,单元测试站在程序员角度从内部测试应用。

同时使用两种测试的工作流程:

(1)先写功能测试,从用户角度描述应用的新功能。

(2)功能测试失败后,想办法写代码让它通过(或者说至少让挡墙失败的测试通过)。此时,使用一个或多个单元测试定义希望代码实现的效果,保证为应用中的每一行代码(只少)编写一个单元测试。

(3)单元测试失败后,编写最少量的应用代码,刚好让单元测试通过。有时,要在第2步和第3步之间多往复,直到我们觉得功能测试有一点进展为止。

(4)然后,再次运行功能测试,看能否通过,或者有没有进展。这一步可能促使我们编写一些新的单元测试和代码等。

3.3Django中的单元测试

lists/tests.py:

from django.test import TestCase


class SmokeTest(TestCase):

    def test_bad_maths(self):
        self.assertEqual(1 + 1, 3)

运行测试:

python manage.py test

Creating test database for alias 'default'...

System check identified no issues (0 silenced).

F

======================================================================

FAIL: test_bad_maths (lists.tests.SmokeTest)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "/.../TDDweb/lists/tests.py", line 7, in test_bad_maths

    self.assertEqual(1 + 1, 3)

AssertionError: 2 != 3

 

----------------------------------------------------------------------

Ran 1 test in 0.001s

 

FAILED (failures=1)

Destroying test database for alias 'default'...

git status

git add lists

git diff --staged

git commit -m 'Add app for lists,with deliberrately failing unit test' 

 

3.4 Django中的MVC、URL和视图函数

测试:

  • 能否解析网站根路径‘/'的URL,将其对应到我们编写的某个视图函数上;
  • 能否让视图函数返回一些HTML,让测试通过
from django.urls import resolve
from django.test import TestCase
from lists.views import home_page

class HomePageTest(TestCase):

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')      #解析url,映射到响应的视图函数
        self.assertEqual(found.func,home_page)

 

ImportError: cannot import name 'home_page'

3.5 编写应用代码

lists/views.py

from django.shortcuts import render


home_page = None

django.urls.exceptions.Resolver404: {'tried': [[ (admin:admin) ^admin/>]], 'path': ''}

 

3.6 urls.py

测试表明需要一个url映射。

TDDweb/urls.py

from django.conf.urls import url
from django.contrib import admin
from lists import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$',views.home_page,name='home')
]

raise TypeError('view must be a callable or a list/tuple in the case of include().')

修改views

def home_page():
    pass

System check identified no issues (0 silenced).

.

----------------------------------------------------------------------

Ran 1 test in 0.001s

 

OK 

 

通过测试

git commit -am 'Fist unit test and url mapping, dummy view'      #-am   添加所有已追踪文件中的改动,而且使用命令行中输入的提交消息

3.7 为视图编写单元测试

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from django.http import HttpRequest

class HomePageTest(TestCase):

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')      #解析url,映射到响应的视图函数
        self.assertEqual(found.func,home_page)

    def test_home_page_returns_correct_html(self):
        request=HttpRequest()
        response = home_page(request)
        html = response.content.decode('utf8')
        self.assertTrue(html.startswith(''))
        self.assertIn('To-Do lists',html)
        self.assertTrue(html.endswith(''))

TypeError: home_page() takes 0 positional arguments but 1 was given

“单元测试/编写代码”循环

小幅改动:

views.py

def home_page(request):
    pass

继续测试:

AttributeError: 'NoneType' object has no attribute 'content'

小幅改动:

views.py

from django.http import HttpResponse


def home_page(request):
    return HttpResponse()

继续测试:

    self.assertTrue(html.startswith(''))

AssertionError: False is not true

继续改动:

def home_page(request):
    return HttpResponse('')

测试:

self.assertIn('To-Do',html)

AssertionError: 'To-Do' not found in ''

继续:

def home_page(request):
    return HttpResponse('To-Do lists')

self.assertTrue(html.endswith(''))

AssertionError: False is not true

def home_page(request):
    return HttpResponse('To-Do lists')

System check identified no issues (0 silenced).

..

----------------------------------------------------------------------

Ran 2 tests in 0.001s

 

OK

单元测试通过

下面启动服务器进行功能测试

======================================================================

FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "functional_tests.py", line 21, in test_can_start_a_list_and_retrieve_it_later

    self.fail('Finish the test!')

AssertionError: Finish the test!

 

----------------------------------------------------------------------

Ran 1 test in 3.799s

 功能测试完成

git commit -am 'Basic view now returns minimal HTML'

git log --oneline

d72e827 Basic view now returns minimal HTML

7ea9484 Fist unit test and url mapping, dummy view

0f12b62 Add app for lists,with deliberrately failing unit test

222dadb first use unittest

a1150e9 first commit

 

第4章 测试及重构的目的

4.2 使用Selenium测试用户交互

from selenium import webdriver
import unittest
from selenium.webdriver.common.keys import Keys
import time


class NewVisitorTest(unittest.TestCase):
    def setUp(self):  # 测试执行前
        self.browser = webdriver.Firefox()

    def tearDown(self):  # 测试方法执行后
        self.browser.quit()

    def test_can_start_a_list_and_retrieve_it_later(self):  # 测试方法
        # 乔伊听说有一个很酷的在线待办事项应用
        # 她去看了这个应用的首页
        self.browser.get('http://localhost:8000')
        # 她注意到网页的标题和头部包含'To-Do'这个词
        self.assertIn('To-Do', self.browser.title)  # 测试失败执行
        header_text = self.browser.find_element_by_tag_name('h1').text
        self.assertIn('To-Do', header_text)

        # 应用邀请她输入一个待办事项
        inputbox = self.browser.find_element_by_id('id_new_item')
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            'Enter a to-do item'
        )

        # 她在文本中输入了'Buy peacock feathers'(购买孔雀羽毛)
        # 她的爱好是钓鱼
        inputbox.send_keys('Buy peacock feathers')

        # 她输入回车键后页面刷新了
        # 待办事项显示'1:Buy peacock feathers'
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        table = self.browser.find_element_by_id('id_list_table')
        rows = table.find_elements_by_tag_name('tr')
        self.assertTrue(
            any(row.text == '1: Buy peacock feathers' for row in rows)
        )

        # 页面又显示了一个文本框,可以输入其他待办事项
        # 她输入了'Use peacock feathers to make a fly'(做假蝇)
        # 她做事非常有条理
        self.fail('Finish the test!')

        # 页面再次刷新,她的清单显示两条事项

        # 她想知道这个网站是否会记住她的清单
        # 她看到网站为她生成了一个唯一的URL
        # 而且页面中有一个文字解说这个功能

        # 她访问那个URL,发现她的清单还在

        # 她很满意,去休息了


if __name__ == '__main__':
    unittest.main(warnings='ignore')  # 禁止抛出ResourceWarning

结果:

selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: h1

提交:

git commit -am 'Functional test now checks we can input to-do item'

4.3 遵守不测试常量规则,使用模板解决这个问题

 

创建“lists/templates/home.html”

改为'To-Do lists'</p> <p> </p> <p>修改views.py</p> <pre class="has"><code class="language-html">from django.shortcuts import render def home_page(request): return render(request, 'home.html')</code></pre> <p>进行单元测试:python manage.py test</p> <blockquote> <p>raise TemplateDoesNotExist(template_name, chain=chain)</p> <p>django.template.exceptions.TemplateDoesNotExist: home.html</p> </blockquote> <p>settings注册app</p> <pre class="has"><code class="language-html">INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'lists', ]</code></pre> <p>继续单元测试发现:</p> <blockquote> <p>self.assertTrue(html.startswith('<html>'))</p> <p>AssertionError: False is not true</p> </blockquote> <p>主要看模板最后是不是空行(\n)</p> <p>修改:</p> <pre class="has"><code class="language-html">self.assertTrue(html.strip().endswith('</html>'))</code></pre> <p>通过测试</p> <p>重构:test.py</p> <pre class="has"><code class="language-python">from django.test import TestCase class HomePageTest(TestCase): def test_use_home_template(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html')</code></pre> <p> </p> <p>git add .   追踪templates</p> <p>git commit -m 'Refactor home page view touse atemplate'</p> <p>4.5 修改首页</p> <p>home.html中加入</p> <pre class="has"><code class="language-html"><h1>Your To-Do list</h1></code></pre> <p>功能测试:python3 functional_tests.py</p> <blockquote> <p>selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_new_item"]</p> </blockquote> <p>加入</p> <pre class="has"><code class="language-html"><input type="text" id="id_new_item"></code></pre> <p>功能测试:</p> <blockquote> <p>AssertionError: '' != 'Enter a to-do item'</p> </blockquote> <p>继续修改:</p> <pre class="has"><code class="language-html"><input type="text" id="id_new_item" placeholder="Enter a to-do item"></code></pre> <p>继续测试:</p> <p>selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_list_table"]</p> <p>加入:</p> <pre class="has"><code class="language-html"><table id="id_list_table"></table></code></pre> <p>测试:</p> <blockquote> <p>any(row.text == '1: Buy peacock feathers' for row in rows)</p> <p>AssertionError: False is not true</p> </blockquote> <p>修改:</p> <pre class="has"><code class="language-html">self.assertTrue( any(row.text == '1: Buy peacock feathers' for row in rows), 'New To-Do item did not appear in table' )</code></pre> <blockquote> <p>'New To-Do item did not appear in table'</p> <p>AssertionError: False is not true : New To-Do item did not appear in table</p> </blockquote> <p>提交:git commit -am 'Front page HTML now generated from a template'</p> <p>4.6 TDD流程</p> <p> </p> <p> </p> <h3>第5章 保存用户输入:测试数据库</h3> <p>5.1 编写表单,发送POST请求</p> <pre class="has"><code class="language-html"><html> <head> <meta charset="UTF-8"> <title>To-Do lists

Your To-Do list

功能测试:python3 functional_tests.py

raise exception_class(message, screen, stacktrace)

selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_list_table"]

延长time.sleep时长,观察网页的调试会发现是CSRF的问题。(settings注册了CSRF中间件)

{% csrf_token %}

 

    'New To-Do item did not appear in table'

AssertionError: False is not true : New To-Do item did not appear in table

 5.2 在服务器中处理POST请求

list/tests.py

from django.test import TestCase


class HomePageTest(TestCase):

    def test_use_home_template(self):
        response = self.client.get('/')
        self.assertTemplateUsed(response, 'home.html')

    def test_can_save_a_POST_request(self):
        response = self.client.post('/', data={'item_text': 'A new list item'})
        self.assertIn('A new list item', response.content.decode())

单元测试:python3 manage.py test 

self.assertIn('A new list item', response.content.decode())

AssertionError: 'A new list item' not found in '\n\n  

 

结果符合预期

先编写一个简单的返回让测试通过

from django.shortcuts import render
from django.http import HttpResponse


def home_page(request):
    if request.method == 'POST':
        return HttpResponse(request.POST['item_text'])
    return render(request, 'home.html')

5.3 把PYTHON变量传入模板中渲染

home.html



    
    To-Do lists


Your To-Do list

{% csrf_token %}
{{ new_item_text }}

调整list/tests.py

def test_can_save_a_POST_request(self):
    response = self.client.post('/', data={'item_text': 'A new list item'})
    self.assertIn('A new list item', response.content.decode())
    self.assertTemplateUsed('response','home.html')

 

AssertionError: No templates used to render the response

views.py

from django.shortcuts import render


def home_page(request):
    return render(request,'home.html',{'new_item_text':request.POST['item_text']})

单元测试:

django.utils.datastructures.MultiValueDictKeyError: "'item_text'" 

 

from django.shortcuts import render


def home_page(request):
    return render(request,'home.html',{'new_item_text':request.POST.get('item_text','')})

单元测试通过

功能测试:

AssertionError: False is not true : New To-Do item did not appear in table 

functional_tests.py代码改进:

(1)

self.assertTrue(
    any(row.text == '1: Buy peacock feathers' for row in rows),
    f'New To-Do item did not appear in table。Contents were:\n{table.text}'
)

3.6的新特性,字符串前加f,里面就可以用“{}”内加 局部变量

AssertionError: False is not true : New To-Do item did not appear in table。Contents were:

Buy peacock feathers

 

(2)

self.assertIn('1: Buy peacok feathers' , [row.text for row in rows])

 

AssertionError: '1: Buy peacock feathers' not found in ['Buy peacock feathers']

手动加上‘1:’  :

1: {{ new_item_text }}

通过功能测试。

继续编写功能测试:

# 页面又显示了一个文本框,可以输入其他待办事项
        # 她输入了'Use peacock feathers to make a fly'(做假蝇)
        # 她做事非常有条理
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use peacock feathers to make a fly')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        # 页面再次刷新,她的清单显示两条事项
        table = self.browser.find_element_by_id('id_list_table')
        rows = table.find_elements_by_tag_name('tr')
        self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
        self.assertIn('2: Use peacock feathers to make a fly ', [row.text for row in rows])

        # 她想知道这个网站是否会记住她的清单
        # 她看到网站为她生成了一个唯一的URL
        # 而且页面中有一个文字解说这个功能
        self.fail('Finish the test!')

 

AssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock feathers to make a fly']

5.4 事不过三,三则重构

重构之前要提交

test开头的方法作为测试方法,其他方法作为辅助方法,辅助方法尽量放在测试方法之前。

 def tearDown(self):  # 测试方法执行后
        self.browser.quit()

    def check_for_row_in_list_table(self, row_text):
        table = self.browser.find_element_by_id('id_list_table')
        rows = table.find_elements_by_tag_name('tr')
        self.assertIn(row_text, [row.text for row in rows])

    def test_can_start_a_list_and_retrieve_it_later(self):  # 测试方法
        # 乔伊听说有一个很酷的在线待办事项应用
        # 她去看了这个应用的首页
        self.browser.get('http://localhost:8000')
        # 她注意到网页的标题和头部包含'To-Do'这个词
        self.assertIn('To-Do', self.browser.title)  # 测试失败执行
        header_text = self.browser.find_element_by_tag_name('h1').text
        self.assertIn('To-Do', header_text)

        # 应用邀请她输入一个待办事项
        inputbox = self.browser.find_element_by_id('id_new_item')
        self.assertEqual(
            inputbox.get_attribute('placeholder'),
            'Enter a to-do item'
        )

        # 她在文本中输入了'Buy peacock feathers'(购买孔雀羽毛)
        # 她的爱好是钓鱼
        inputbox.send_keys('Buy peacock feathers')

        # 她输入回车键后页面刷新了
        # 待办事项显示'1:Buy peacock feathers'
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)
        self.check_for_row_in_list_table('1: Buy peacock feathers')

        # 页面又显示了一个文本框,可以输入其他待办事项
        # 她输入了'Use peacock feathers to make a fly'(做假蝇)
        # 她做事非常有条理
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Use peacock feathers to make a fly')
        inputbox.send_keys(Keys.ENTER)
        time.sleep(1)

        # 页面再次刷新,她的清单显示两条事项
        table = self.browser.find_element_by_id('id_list_table')
        rows = table.find_elements_by_tag_name('tr')
        self.check_for_row_in_list_table('1: Buy peacock feathers')
        self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')

AssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock feathers to make a fly']

测试一致,证明完成重构,做一次提交:git commit -am '第一次重构完成' 

 

5.5 Django ROM 第一个模型

list/tests.py

from lists.models import Item


[...]

class ItemModelTest(TestCase):
    def test_saving_and_retrieving_items(self):
        first_item = Item()
        first_item.text = 'The first (ever) list item'
        first_item.save()

        second_item = Item()
        second_item.text = 'Item the second'
        second_item.save()

        saved_items = Item.objects.all()
        self.assertEqual(saved_items.count(), 2)

        first_saved_item = saved_items[0]
        second_saved_item = saved_items[1]
        self.assertEqual(first_saved_item.text, 'The first (ever) list item')
        self.assertEqual(second_saved_item.text, 'Item the second')

单元测试: 

ImportError: cannot import name 'Item' 

list/models.py

from django.db import models

class Item:
    pass

继续单元测试:

AttributeError: 'Item' object has no attribute 'save'

实现save方法:

from django.db import models

class Item(models.Model):
    pass

继续单元测试:

django.db.utils.OperationalError: no such table: lists_item 

5.5.1 第一个数据迁移 

ORM的任务是模型化数据库。创建数据库是由迁移负责。迁移 的任务是根据models.py的变化,变动数据库的表。

创建迁移:python manage.py makemigrations

Migrations for 'lists':

  lists/migrations/0001_initial.py

    - Create model Item

再次测试:

AttributeError: 'Item' object has no attribute 'text' 

修改models.py

class Item(models.Model):
    text = models.TextField()

django.db.utils.OperationalError: no such column: lists_item.text

 5.5.3 添加新字段就要创建新迁移

You are trying to add a non-nullable field 'text' to item without a default; we can't do that (the database needs something to populate existing rows).

Please select a fix:

 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)

 2) Quit, and let me add a default in models.py

Select an option: 2  

这个命令不允许添加没有默认值的列。

class Item(models.Model):
    text = models.TextField(default='')

  lists/migrations/0002_item_text.py

    - Add field text to item

测试通过

git add lists

git commit -m 'Models for list Items and associated migration'

5.6 把POST请求中的数据存入数据库

增加tests.py代码

def test_can_save_a_POST_request(self):
    response = self.client.post('/', data={'item_text': 'A new list item'})

    self.assertEqual(Item.objects.count(), 1)
    new_item = Item.objects.first()
    self.assertEqual(new_item.text, 'A new list item')

    self.assertIn('A new list item', response.content.decode())
    self.assertTemplateUsed(response, 'home.html')

 代码异味:POST测试请求太长

AssertionError: 0 != 1

修改视图:

from django.shortcuts import render
from lists.models import Item


def home_page(request):
    item = Item()
    item.text = request.POST.get('item_text', '')
    item.save()
    
    return render(request, 'home.html', {'new_item_text': request.POST.get('item_text', '')})

需要进一步处理:

1、代码异味:POST测试请求太长

2、每次请求都会保存一个空白的待办事项 

return render(request, 'home.html', {'new_item_text': item.text})

处理2、每次请求都会保存一个空白的待办事项:

class HomePageTest(TestCase):
    [...]

    def test_only_saves_items_when_necessary(self):
        self.client.get('/')
        self.assertEqual(Item.objects.count(), 0)

AssertionError: 1 != 0

 

修改视图:

def home_page(request):
    if request.method == 'POST':
        new_item_text = request.POST['item_text']
        Item.objects.create(text=new_item_text)    #简化Item对象的创建,无需使用save
    else:
        new_item_text = ''

    return render(request, 'home.html', {'new_item_text': new_item_text})

通过测试

5.7 处理POST请求后重定向

def test_can_save_a_POST_request(self):
    response = self.client.post('/', data={'item_text': 'A new list item'})

    self.assertEqual(Item.objects.count(), 1)
    new_item = Item.objects.first()
    self.assertEqual(new_item.text, 'A new list item')

    self.assertEqual(response.status_code, 302)
    self.assertEqual(response['location'], '/')

 

AssertionError: 200 != 302

 

清理视图:

from django.shortcuts import render, redirect
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/')

    return render(request, 'home.html')

通过单元测试

更好的单元测试实践方法:一个测试只测试一件事情

    def test_can_save_a_POST_request(self):
        response = self.client.post('/', data={'item_text': 'A new list item'})

        self.assertEqual(Item.objects.count(), 1)
        new_item = Item.objects.first()
        self.assertEqual(new_item.text, 'A new list item')

    def test_redirects_after_POST(self):
        response = self.client.post('/', data={'item_text': 'A new list item'})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/')

Ran 5 tests in 0.015s

OK

 

5.8 在模板中渲染待办事项

待处理事项:

在表格中显示多个待办事项

支持多个清单

设置-使用-断言   : 测试的典型结构


 

    def test_display_all_list(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        response = self.client.get('/')

        self.assertIn('itemey 1',response.content.decode())
        self.assertIn('itemey 2',response.content.decode())

 

AssertionError: 'itemey 1' not found in '\n\n

修改模板和视图


    {% for item in items %}
    
    {% endfor %}
1: {{ item.text }}
from django.shortcuts import render, redirect
from lists.models import Item


def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/')

    items = Item.objects.all()
    return render(request, 'home.html',{'items':items})

Ran 6 tests in 0.021s

OK

单元测试通过

功能测试:AssertionError: 'To-Do' not found in 'OperationalError at /'

5.9 使用迁移创建生产数据库

Django为单元测试创建了一个专用的测试数据库。所以下一步要创建一个真正的数据库通过功能测试。

默认使用sqlite3

创建数据库:python manage.py migrate

功能测试:

AssertionError: '2: Use peacock feathers to make a fly ' not found in ['1: bsdfa', '1: Buy peacock feathers', '1: Use peacock feathers to make a fly']

{% for item in items %}
{{ forloop.counter }}: {{ item.text }}
{% endfor %}

AssertionError: '1: Buy peacock feathers' not found in ['1: bsdfa', '2: Buy peacock feathers', '3: Use peacock feathers to make a fly', '4: Buy peacock feathers']

 每次测试都会在数据库中遗留数据。

手动清理:先删除数据库,再创建

rm db.sqlite3

python manage.py migrate --noinput

 

    self.fail('Finish the test!')

AssertionError: Finish the test!

功能测试通过 

待处理事项:

功能测试之后删除测试数据

支持多个清单

提交:

git add lists

git commit -m 'Redirect after POST, and show all items in template'

 

 

第6章 改进功能测试:确保隔离,去掉含糊的休眠

 

6.1 确保功能测试之间相互隔离

将功能测试的运行方式改为单元测试相同的运行方式

mkdir functional_tests

touch functional_tests/__init__.py

git mv functional_tests.py functional_tests/tests.py

现在运行功能测试的方式改为:Python manage.py test functional_tests

修改functional_tests/tests.py

删掉main

from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time


class NewVisitorTest(LiveServerTestCase):
    def setUp(self):  # 测试执行前
        self.browser = webdriver.Firefox()


    def test_can_start_a_list_and_retrieve_it_later(self):  # 测试方法
        # 乔伊听说有一个很酷的在线待办事项应用
        # 她去看了这个应用的首页
        self.browser.get(self.live_server_url)

运行:Python manage.py test functional_tests

没有问题,提交

git status

git add functional_tests

git diff --staged -M

git commit -m 'make functional_tests as app ,use LiveServerTestCase'

 

运行python manage.py test     会运行所有的测试APP

如果只想运行单元测试:Python manage.py test lists

6.2 升级selenium和Geckodriver

pip install --upgrade selenium

下载新版geckodriver,备份旧版,执行geckodriver --version 查看是否可用

6.3 隐式等待、显示等待和含糊的time.sleep

selenium内置了显示等待和隐式等待,隐式等待不建议使用。

from selenium.common.exceptions import WebDriverException

MAX_WAIT = 10


class NewVisitorTest(LiveServerTestCase):
    def setUp(self):  # 测试执行前
        self.browser = webdriver.Firefox()

    def tearDown(self):  # 测试方法执行后
        self.browser.quit()

    def wait_for_row_in_list_table(self, row_text):
        start_time = time.time()
        while True:
            try:
                table = self.browser.find_element_by_id('id_list_table')
                rows = table.find_elements_by_tag_name('tr')
                self.assertIn(row_text, [row.text for row in rows])
                return
            except (AssertionError, WebDriverException) as e:
                if time.time() - start_time > MAX_WAIT:
                    raise e
                time.sleep(0.5)

删除time.sleep()

# 她输入回车键后页面刷新了
# 待办事项显示'1:Buy peacock feathers'
inputbox.send_keys(Keys.ENTER)
self.wait_for_row_in_list_table('1: Buy peacock feathers')

# 页面又显示了一个文本框,可以输入其他待办事项
# 她输入了'Use peacock feathers to make a fly'(做假蝇)
# 她做事非常有条理
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys('Use peacock feathers to make a fly')
inputbox.send_keys(Keys.ENTER)
time.sleep(1)

# 页面再次刷新,她的清单显示两条事项
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.wait_for_row_in_list_table('1: Buy peacock feathers')
self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')

运行:Python manage.py test

破坏测试:

(1)

rows = table.find_elements_by_tag_name('tr')
self.assertIn('foo', [row.text for row in rows])

AssertionError: 'foo' not found in ['1: Buy peacock feathers']

(2)

table = self.browser.find_element_by_id('id_nothing')
rows = table.find_elements_by_tag_name('tr')

selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_nothing"]

还原,测试一下看看有没有问题

AssertionError: Finish the test!

 

你可能感兴趣的:(Python学习,测试驱动开发)