测试驱动开发(Django)14

第14章 简单的表单

Django鼓励使用表单类验证用户输入和显示错误信息。

14.1 把验证逻辑移动到表单中

在Django中视图很复杂则说明代码异味。考虑如何把逻辑移动到表单或模型类中。

Django表单的功能:

  • 可以处理用户输入并加以验证
  • 可以在模板中使用,并且有不同的渲染以及错误消息
  • 可以把数据存入数据库

14.1.1 使用单元测试探索表单API

lists/forms.py

from django import forms

class ItemForm(forms.Form):
    item_text = forms.CharField()

lists/tests/test_forms.py

from django.test import TestCase
from lists.forms import ItemForm

class ItemFormTest(TestCase):
    def test_form_renders_tem_text_input(self):
        form = ItemForm()
        self.fail(form.as_p())

 

测试:AssertionError: <p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>

lists/tests/test_forms.py

class ItemFormTest(TestCase):

    def test_form_item_input_has_placeholder_and_css_classes(self):
        form = ItemForm()
        self.assertIn('placeholder="Enter a to-do item"', form.as_p())
        self.assertIn('class="form-control input-lg"', form.as_p())

AssertionError: <p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>

lists/forms.py

    widget = forms.fields.TextInput(attrs={
        'placeholder': 'Enter a to-do item',
        'class':'form-control input-lg'
    })

AssertionError: 'placeholder="Enter a to-do item"' not found in '<p><label for="id_item_text">Item text:</label> <input type="text" name="item_text" required id="id_item_text" /></p>'

 

14.1.2 换用Django中的ModelForm类

表单可用通过Django提供的ModelForm类来重用已经在模型中定义好的验证规则。

from django import forms
from lists.models import Item


class ItemForm(forms.models.ModelForm):

    class Meta:
        model = Item        #指定表单应用模型
        fields = ('text',)   #使用字段

AssertionError: 'placeholder="Enter a to-do item"' not found in '<p><label for="id_text">Text:</label> <textarea name="text" cols="40" rows="10" required id="id_text">\n</textarea></p>'

 


class ItemForm(forms.models.ModelForm):
    class Meta:
        model = Item  # 指定表单应用模型
        fields = ('text',)  # 使用字段
        widgets = {
            'text': forms.fields.TextInput(
                attrs={
                    'placeholder': 'Enter a to-do item',
                    'class': 'form-control input-lg',
                }
            ),
        }

通过测试

 

14.1.3 测试和定制表单验证

lists/tests/test_forms.py增加用一个测试

def test_form_validation_for_blank_items(self):
    form = ItemForm(data={'text':''})
    form.save()

ValueError: The Item could not be created because the data didn't validate.

def test_form_validation_for_blank_items(self):
    form = ItemForm(data={'text':''})
    self.assertFalse(form.is_valid())
    self.assertEqual(
        form.errors['text'],
        ["You can't have an empty list item"]
    )

AssertionError: ['This field is required.'] != ["You can't have an empty list item"]

那就修改默认错误提示

lists/forms.py

class Meta:
    model = Item  # 指定表单应用模型
    fields = ('text',)  # 使用字段
    widgets = {
        'text': forms.fields.TextInput(
            attrs={
                'placeholder': 'Enter a to-do item',
                'class': 'form-control input-lg',
            }
        ),
    }
    error_messages = {
        'text':{'required':"You can't have an empty list item"}
    }

通过测试

使用常量避免错误消息搅乱代码

EMPTY_ITEM_ERROR = "You can't have an empty list item"
[...]
        error_messages = {
            'text':{'required':EMPTY_ITEM_ERROR}
        }

修改测试

from lists.forms import ItemForm,EMPTY_ITEM_ERROR
[...]
        self.assertEqual(
            form.errors['text'],[EMPTY_ITEM_ERROR]  
        )

通过测试

提交: git commit -am 'new form for list items'

 

14.2 在视图中使用这个表单

14.2.1 在处理GET请求的视图中使用这个表单

lists/tests/test_views.py

from lists.forms import ItemForm


class HomePageTest(TestCase):

    def test_use_home_template(self):
        response = self.client.get('/')
        self.assertTemplateUsed(response, 'home.html')
    def test_home_page_uses_item_form(self):
        response = self.client.get('/')
        self.assertIsInstance(response.context['form'],ItemForm)

测试结果:KeyError: 'form'

修改视图:

from lists.forms import ItemForm


def home_page(request):
    return render(request, 'home.html',{'form':ItemForm()})

替换base.html input

<form method="POST" action="{% block form_action %}{% endblock %}">
    {% csrf_token %}
    {{ form.text }}
    {% if error %}

14.2.2 大量查找和替换

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

提交以区分重命名和逻辑变动:git commit -am 'use new form in home_page,simplify tests. NB breaks stuff'

base.py 增加一个新辅助方法

def get_item_input_box(self):
    return self.browser.find_element_by_id('id_text')

使用get_item_input_box方法替换掉功能测试中id_new_item

grep -Ir item_text lists/       找到之后替换为text

grep -r id_new_item lists/  找到之后替换为id_text

通过单元测试

Ran 17 tests in 0.063s

OK

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

14.3 在POST请求的视图中使用这个表单

14.3.1 修改new_list视图的单元测试

lists/tests/test_views.py

分拆 test_validation_errors_are_sent_back_to_home_page_template方法

class NewListTest(TestCase):
    def test_for_invalid_input_renders_home_template(self):
        response = self.client.post('/lists/new', data={'text': ''})
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'home.html')

    def test_validation_errors_are_shown_on_home_page(self):
        response = self.client.post('/lists/new', data={'text': ''})
        self.assertContains(response, escape(EMPTY_ITEM_ERROR))

    def test_for_invalid_input_passes_form_to_template(self):
        response = self.client.post('/lists/new',data={'text':''})
        self.assertIsInstance(response.context['form'],ItemForm)

单元测试:

KeyError: 'form'

14.3.2 在视图中使用这个表单

视图:

def new_list(request):
    form = ItemForm(data=request.POST)
    if form.is_valid():
        list_ = List.objects.create()
        Item.objects.create(text=request.POST['text'], list=list_)
        return redirect(list_)
    else:
        return render(request, 'home.html', {'form': form})

单元测试:AssertionError: False is not true : Couldn't find 'You can&#39;t have an empty list item' in response

14.3.3 使用这个表单在模板中显示错误消息

base.html

<form method="POST" action="{% block form_action %}{% endblock %}">
    {% csrf_token %}
    {{ form.text }}
    {% if form.errors %}
        <div class="form-group has-error">
            <div class="help-block">{{ form.text.errors }}</div>
        </div>
    {% endif %}
</form>

单元测试:AssertionError: False is not true : Couldn't find 'You can&#39;t have an empty list item' in response

14.4 在其他视图中使用这个表单

lists/tests/test_views.py增加一个新的测试

class ListViewTest(TestCase):
[...]
    ​​​​​​​def test_displays_item_form(self):
        list_ = List.objects.create()
        response = self.client.get(f'/lists/{list_.id}/')
        self.assertIsInstance(response.context['form'], ItemForm)
        self.assertContains(response, 'name="text"')

KeyError: 'form' ,解决错误:修改视图

def view_list(request, list_id):
[...]
    form = ItemForm()
    return render(request, 'list.html', {'list': list_, 'form':form,'error': error})

14.4.1 定义辅助方法,简化测试

lists/tests/test_views.py

分拆test_validation_errors_end_up_on_lists_page方法

   def post_invalid_input(self):
        list_ = List.objects.create()
        return self.client.post(
            f'/lists/{list_.id}/',
            data={'text': ''}
        )

    def test_for_inwalid_input_noting_saved_to_db(self):
        self.post_invalid_input()
        self.assertEqual(Item.objects.count(), 0)

    def test_for_invalid_input_renders_list__template(self):
        response = self.post_invalid_input()
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'list.html')

    def test_for_invalid_input_passes_form_template(self):
        response = self.post_invalid_input()
        self.assertIsInstance(response.context['form'], ItemForm)

    def test_for_invalid_input_shows_error_on_page(self):
        response = self.post_invalid_input()
        self.assertContains(response, escape(EMPTY_ITEM_ERROR))

测试结果:AssertionError: False is not true : Couldn't find 'You can&#39;t have an empty list item' in response

视图:

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    error = None
    form = ItemForm(data=request.POST)
    if request.method == 'POST':
        form = ItemForm()
        if form.is_valid():
            Item.objects.create(text=request.POST['text'], list=list_)
            return redirect(list_)

    return render(request, 'list.html', {'list': list_, 'form': form})

单元测试:

Ran 23 tests in 0.085s

OK

功能测试:selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: .has-error

14.4.2 意想不到的好处:HTML5自带的客户端验证

Django为输入框添加了required属性,HTML5的特性,浏览器会在客户端做验证,输入无效禁止提交表单。

python manage.py test functional_tests

Ran 4 tests in 43.807s

OK

 

14.5 提交修改

git commit -am 'use form in all views, back to working state'

14.6 自定制错误并非无意义,因为并不是所有的浏览器都自带验证

14.7 使用表单自带的save方法

lists/tests/test_forms.py

def test_form_save_handles_saving_to_a_list(self):
    list_ = List.objects.create()
    form = ItemForm(data={'text':'do me'})
    new_item = form.save(for_list = list_)
    self.assertEqual(new_item,Item.objects.first())
    self.assertEqual(new_item.text,'do me')
    self.assertEqual(new_item.list,list_)

TypeError: save() got an unexpected keyword argument 'for_list'

lists/forms.py

def save(self, for_list):
    self.instance.list = for_list
    return super().save()

 

Ran 24 tests in 0.084s

OK

修改视图:

def new_list(request):
    form = ItemForm(data=request.POST)
    if form.is_valid():
        list_ = List.objects.create()
        form.save(for_list=list_)
        return redirect(list_)
    else:
        return render(request, 'home.html', {'form': form})

Ran 24 tests in 0.084s

OK

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    form = ItemForm()
    if request.method == 'POST':
        form = ItemForm(data=request.POST)
        if form.is_valid():
            form.save(for_list=list_)
            return redirect(list_)

    return render(request, 'list.html', {'list': list_, 'form': form})

Ran 24 tests in 0.083s

OK

功能测试:

Ran 4 tests in 33.001s

OK

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值