测试驱动开发(Django)7

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Marvin_Wind/article/details/86816164

第7章 步步为营

7.1 必要时做少量的设计

7.1.1 不要预先做大量设计

7.1.2 YAGNI

7.1.3 REST

本章便签 :

  • 调整模型,让待办事项和不同的清单关联起来
  • 为每个清单添加唯一的URL
  • 添加通过POST请求新建清单所需URL
  • 添加通过POST请求在现有的清单中增加新待办事项所需的URL

7.2 使用TDD实现新设计

7.3 确保出现回归测试

引入第二个用户,确认他的待办事项清单与第一个用户是分开的。

        self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')
        self.wait_for_row_in_list_table('1: Buy peacock feathers')

   def test_multiple_users_can_start_lists_at_different_urls(self):
        # 乔伊新建一个待办事项清单
        self.browser.get(self.live_server_url)
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Buy peacock feathers')
        inputbox.send_keys(Keys.ENTER)
        self.wait_for_row_in_list_table('1: Buy peacock feathers')

        # 她注意到清单有个唯一的URL
        edith_list_url = self.browser.current_url
        self.assertRegex(edith_list_url, '/lists/.+')

        # 现在一名焦作弗朗西斯的新用户访问了网站

        ## 我们使用了一个新浏览器会话
        ## 确保乔伊的信息不会从cookie中泄露出去
        self.browser.quit()
        self.browser = webdriver.Firefox()

        # 弗朗西斯访问首页
        # 页面中看不到乔伊的清单
        self.browser.get(self.live_server_url)
        page_text = self.browser.find_element_by_tag_name('body').text
        self.assertNotIn('Buy peacock feathers', page_text)
        self.assertNotIn('make a fly', page_text)

        # 弗朗西斯输入一个新待办事项,新建一个清单
        # 他不像乔伊那样兴趣盎然
        inputbox = self.browser.find_element_by_id('id_new_item')
        inputbox.send_keys('Buy milk')
        inputbox.send_keys(Keys.ENTER)
        self.wait_for_row_in_list_table('1: Buy milk')

        # 弗朗西斯获得了他的唯一URL
        francis_list_url = self.browser.current_url
        self.assertRegex(francis_list_url, '/lists/.+')
        self.assertNotEqual(francis_list_url, edith_list_url)

        # 这个页面还是没有乔伊的清单
        page_text = self.browser.find_element_by_tag_name('body').text
        self.assertNotIn('Buy peacock feathers', page_text)
        self.assertIn('Buy milk', page_text)
        
        #两个人都很满意,然后去休息了

self.assertRegex(edith_list_url, '/lists/.+')

AssertionError: Regex didn't match: '/lists/.+' not found in 'http://localhost:49669/'

提交:git commit -m 'second user functional_test'

7.4 逐步迭代,实现新设计

lists/tests.py

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'], '/lists/the-only-list-in-the-world/')

功能测试:AssertionError: '/' != '/lists/the-only-list-in-the-world/'

修改视图

def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/lists/the-only-list-in-the-world/')

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

7.5 自成一体的第一步:新的URL

lists/tests.py 添加新的测试类

class ListViewTest(TestCase):
    def test_display_all_items(self):
        Item.objects.create(text='itemey 1')
        Item.objects.create(text='itemey 2')

        respinse = self.client.get('/lists/the-only-list-in-the-world/')

        self.assertContains(respinse, 'itemey 1')
        self.assertContains(respinse, 'itemey 2')
        

7.5.1 一个新URL

urlpatterns = [
    url(r'^$', views.home_page, name='home'),
    url(r'^lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),
]

单元测试:AttributeError: module 'lists.views' has no attribute 'view_list'

7.5.2 一个新的视图函数

def view_list(request):
    pass

单元测试:ValueError: The view lists.views.view_list didn't return an HttpResponse object. It returned None instead.

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

通过单元测试

功能测试:AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do list\n1: Buy peacock feathers'

根据经验,当所有单元测试都能通过而功能测试不能通过时,问题通常是由单元测试没有覆盖的事物引起的--这往往是模板的问题。

<form method="POST" action="/">

AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do list\n1: Buy peacock feathers'

7.6 变绿了吗?该重构了

删除lists/tests.py

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())

首页不需要显示所有待办事项,只显示一个输入框

7.7 再迈一小步:一个新模板,用于查看清单

首页和清单视图是不同页面,就应该使用不同模板。

检查是否使用不同模板:

lists/tests.py

class ListViewTest(TestCase):
    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')

    def test_display_all_items(self):

修改视图

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

单元测试:django.template.exceptions.TemplateDoesNotExist: list.html

创建list.html  (pycharm创建会提示是否git追加)

单元测试AssertionError: False is not true : Couldn't find 'itemey 1' in response

复制home.html到list.html

通过单元测试

进行重构

home.html

<body>
<h1>Start a new To-Do list</h1>
<form method="POST">
    {% csrf_token %}
    <input type="text" id="id_new_item" placeholder="Enter a to-do item" name="item_text">
</form>
</table>
</body>

视图:

def home_page(request):
    if request.method == 'POST':
        Item.objects.create(text=request.POST['item_text'])
        return redirect('/lists/the-only-list-in-the-world/')

    return render(request, 'home.html')

单元测试没有问题,运行功能测试:

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

提交:git commit -m 'new URL ,view and template to display lists'

 

7.8 第三小步:用于添加待办事项的URL

7.8.1 用来测试新建清单的测试类

lists/tests.py  把test_can_seve_a_POST_request 和test_redirects_after_POST移动到一个新类中,修改请求URL

class NewListTest(TestCase):
    
    def test_can_save_a_POST_request(self):
        response = self.client.post('/lists/new', 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('/lists/new', data={'item_text': 'A new list item'})
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')

改进:

def test_redirects_after_POST(self):
    response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
    self.assertRedirects(response, '/lists/the-only-list-in-the-world/')

 单元测试:

AssertionError: 404 != 302 : Response didn't redirect as expected: Response code was 404 (expected 302)

7.8.2 用于新建清单的URL和视图

urlpatterns = [
    url(r'^$', views.home_page, name='home'),
    url(r'^lists/new$', views.new_list, name='new_list'),
    url(r'^lists/the-only-list-in-the-world/$', views.view_list, name='view_list'),
]

 单元测试:

AttributeError: module 'lists.views' has no attribute 'new_list'

视图:

def new_list(request):
    pass

ValueError: The view lists.views.new_list didn't return an HttpResponse object. It returned None instead.

def new_list(request):
    return redirect('/lists/the-only-list-in-the-world/')

AssertionError: 0 != 1

def new_list(request):
    Item.objects.create(text=request.POST['item_text'])
    return redirect('/lists/the-only-list-in-the-world/')

Ran 7 tests in 0.024s

 

OK

7.8.3 删除当前多余的代码和测试

def home_page(request):
    return render(request, 'home.html')

删掉

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

Ran 6 tests in 0.020s

 

OK

7.8.4 出现回归!让表单指向刚添加的新URL

功能测试:

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

home.html,lists.html共同修改:

<form method="POST" action="/lists/new">

 

AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy milk']\

提交:git commit -am 'commit 7.8'

待办清单:

  • 调整模型,让待办事项和不同的清单关联起来
  • 为每个清单添加唯一的URL
  • 添加通过POST请求新建清单所需URL
  • 添加通过POST请求在现有的清单中增加新待办事项所需的URL

 

7.9 下定决心,调整模型

from django.test import TestCase
from lists.models import Item, List


class HomePageTest(TestCase):

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


class ListAndItemModelTest(TestCase):
    def test_saving_and_retrieving_items(self):
        list_ = List()
        list_.save()

        first_item = Item()
        first_item.text = 'The first (ever) list item'
        first_item.list = list_
        first_item.save()

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

        saved_list = List.objects.all()
        self.assertEqual(saved_list, list_)

        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(first_saved_item.list, list_)
        self.assertEqual(second_saved_item.text, 'Item the second')
        self.assertEqual(second_saved_item.list, list_)

 

单元测试:ImportError: cannot import name 'List' 

7.9.1 外键关系


from django.db import models

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

class List(models.Model):
    pass

python manage.py makemigrations

AssertionError: 'List object' != <List: List object>

 

from django.db import models

class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.ForeignKey(List, default=None)

删除之前的rm lists/migrations/0003_auto_20190209_1304.py
重新迁移python manage.py makemigrations

7.9.2 根据新模型定义调整其他代码

django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

lists/tests.py

def test_display_all_items(self):
    list_ = List.objects.create()
    Item.objects.create(text='itemey 1', list=list_)
    Item.objects.create(text='itemey 2', list=list_)

视图:

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/the-only-list-in-the-world/')

通过单元测试

功能测试:AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buy milk']

git commit -m 'commit 7.9.2'

 

  • 调整模型,让待办事项和不同的清单关联起来
  • 为每个清单添加唯一的URL
  • 添加通过POST请求新建清单所需URL
  • 添加通过POST请求在现有的清单中增加新待办事项所需的URL

 

7.10 每个列表都应该有自己的URL

修改ListViewTest,让两个测试指向新URL。

test_display_all_items改为test_displays_only_items_for_that_list
class ListViewTest(TestCase):
    def test_uses_list_template(self):
        list_ = List.objects.create()
        response = self.client.get(f'/lists/{list_.id}/')  # 3.6新功能
        self.assertTemplateUsed(response, 'list.html')

    def test_displays_only_items_for_that_list(self):
        correct_list = List.objects.create()
        Item.objects.create(text='itemey 1', list=correct_list)
        Item.objects.create(text='itemey 2', list=correct_list)
        other_list = List.objects.create()
        Item.objects.create(text='other list item 1', list=other_list)
        Item.objects.create(text='other list item 2', list=other_list)

        respinse = self.client.get(f'/lists/{correct_list.id}/')

        self.assertContains(respinse, 'itemey 1')
        self.assertContains(respinse, 'itemey 2')
        self.assertNotContains(respinse, 'other list item 1')
        self.assertNotContains(respinse, 'other list item 2')

单元测试:

AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

AssertionError: No templates used to render the response

 

7.10.1 捕获URL中的参数

urlpatterns = [
    url(r'^$', views.home_page, name='home'),
    url(r'^lists/new$', views.new_list, name='new_list'),
    url(r'^lists/(.+)/$', views.view_list, name='view_list'),
]

TypeError: view_list() takes 1 positional argument but 2 were given

视图:

def view_list(request,list_id):

AssertionError: 1 != 0 : Response should not contain 'other list item 1'

视图:

def view_list(request,list_id):
    list_ = List.objects.get(id=list_id)
    items = Item.objects.filter(list=list_)
    return render(request, 'list.html', {'items': items})

ValueError: invalid literal for int() with base 10: 'the-only-list-in-the-world'

7.10.2 按照新设计调整new_list视图

修改lists/tests.py

def test_redirects_after_POST(self):
    response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
    new_list = List.objects.first()
    self.assertRedirects(response, f'/lists/{new_list.id}/')

视图

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect(f'/lists/{list_.id}/')

 

通过单元测试

7.11 功能测试又检测到回归

功能测试:AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use peacock feathers to make a fly']

  • 调整模型,让待办事项和不同的清单关联起来
  • 为每个清单添加唯一的URL
  • 添加通过POST请求新建清单所需URL
  • 添加通过POST请求在现有的清单中增加新待办事项所需的URL

7.12 还需要一个视图,把待办事项加入现有清单

    def test_can_save_a_POSE_request_to_an_existing_list(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        self.client.post(
            f'/lists/{correct_list.id}/add_item',
            data={'item_text': 'A new item for an existing list'}
        )

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

    def test_redirects_to_list_view(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        response = self.client.post(
            f'/lists/{correct_list.id}/add_item',
            data={'item_text': 'A new item for an existing list'}
        )

        self.assertRedirects(response, f'/lists/{correct_list.id}/')

单元测试:AssertionError: 301 != 302 : Response didn't redirect as expected: Response code was 301 (expected 302)

7.12.1 小心霸道的正则表达式

url(r'^lists/(\d+)/$', views.view_list, name='view_list')

AssertionError: 404 != 302 : Response didn't redirect as expected: Response code was 404 (expected 302)

7.12.2 z最后一个新URL

urlpatterns = [
    url(r'^$', views.home_page, name='home'),
    url(r'^lists/new$', views.new_list, name='new_list'),
    url(r'^lists/(\d+)/$', views.view_list, name='view_list'),
    url(r'^lists/(\d+)/add_item$', views.add_item, name='add_item'),
]
  • 调整模型,让待办事项和不同的清单关联起来
  • 为每个清单添加唯一的URL
  • 添加通过POST请求新建清单所需URL
  • 添加通过POST请求在现有的清单中增加新待办事项所需的URL
  • 重构urls.py,去除重复

AttributeError: module 'lists.views' has no attribute 'add_item'

 

7.12.3 最后一个新视图

def add_item(request):
    pass

 

TypeError: add_item() takes 1 positional argument but 2 were given

 

def add_item(request, list_id):
    pass

ValueError: The view lists.views.add_item didn't return an HttpResponse object. It returned None instead.

 

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    return redirect(f'/lists/{list_.id}/')

self.assertEqual(Item.objects.count(), 1)

AssertionError: 0 != 1

 

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect(f'/lists/{list_.id}/')

通过单元测试

7.12.4 直接测试响应上下文对象

lists/tests.py

def test_passes_correct_list_to_tmplate(self):
    other_list = List.objects.create()
    correct_list = List.objects.create()
    response = self.client.get(f'/lists/{correct_list.id}/')
    self.assertEqual(response.context['list'], correct_list)

list.html

<form method="POST" action="/lists/{{ list.id }}/add_item">

KeyError: 'list'

 

视图:

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    return render(request, 'list.html', {'list': list_})

AssertionError: False is not true : Couldn't find 'itemey 1' in response

 

修改list.html

<table id="id_list_table">
    {% for item in list.item_set.all %}
    <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
    {% endfor %}
</table>

通过单元测试

通过功能测试

7.13 使用URL引入做最后一次重构

如果某些URL只在lists应用中使用,Django建议使用单独的lists/urls.py.

TDDweb/urls.py

from django.conf.urls import url,include
from django.contrib import admin
from lists import views
from lists import urls as list_urls

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

TDDweb/urls.py

urlpatterns = [
    url(r'^new$', views.new_list, name='new_list'),
    url(r'^(\d+)/$', views.view_list, name='view_list'),
    url(r'^(\d+)/add_item$', views.add_item, name='add_item'),
]

通过所有测试

 

展开阅读全文

没有更多推荐了,返回首页