CSRF 令牌干扰 TDD - 是否有存储 csrf 输出的变量?
CRSF Token Interfering With TDD - Is there a variable that stores csrf output?
因此,在使用表单输入比较预期与实际 html 时,我一直在 Django 中返回失败测试,所以我打印出结果并意识到差异是相当简单的一行,由我的 {% csrf_token %}
,如下:
<input type='hidden' name='csrfmiddlewaretoken' value='hrPLKVOlhAIXmxcHI4XaFjqgEAMCTfUa' />
所以,我希望得到一个简单的答案,但我没能找到:
如何呈现 csrf_token 的结果以用于测试?
这是测试设置和失败:
def test_home_page_returns_correct_html_with_POST(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertIn('A new list item', response.content.decode())
expected_html = render_to_string(
'home.html',
{'new_item_text': 'A new list item'},
******this is where I'm hoping for a simple one-line mapping******
)
self.assertEqual(response.content.decode(), expected_html)
这是来自 views.py 的效果图:
def home_page(request):
return render(request, 'home.html', {
'new_item_text': request.POST.get('item_text'),
})
这是测试失败,当我 运行 使用 python manage.py test
进行测试时
FAIL: test_home_page_returns_correct_html_with_POST (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Me\PycharmProjects\superlists\lists\tests.py", line 29, in test_home_page_returns_correct_html_with_POST
self.assertEqual(response.content.decode(), expected_html)
AssertionError: '<!DO[298 chars] <input type=\'hidden\' name=\'csrfmiddlew[179 chars]tml>' != '<!DO[298 chars] \n </form>\n\n <table
id="id_list_t[82 chars]tml>'
----------------------------------------------------------------------
CSRF 令牌是模板上下文数据的一部分,如果您使用 Django TestCase 类:
response = self.client.get(url)
print(response.context)
https://docs.djangoproject.com/en/1.9/topics/testing/tools/#django.test.Response
关键是csrf_token
.
https://docs.djangoproject.com/en/1.9/_modules/django/template/context_processors/
编辑:
正如您所问如何将测试中呈现的 HTML 与测试服务器的输出进行比较:
因为您在模板中使用 {% csrf_token %}
,所以您无法将响应上下文中的 CSRF 令牌提供给 render_to_string
方法以使其使用相同的值。相反,您必须在 render_to_string
的结果中替换它,也许首先使用 selenium 查找输入元素(使其本身成为测试)。然而,这个测试有多大用处值得怀疑。它只会帮助确保存在 CSRF 令牌,但无论如何它已经在服务器上以正常工作模式进行了检查。
基本上,您应该测试您在代码中直接影响的任何内容,而不是 Django 魔法提供的任何内容。例如。如果你正在做自定义表单验证,你应该测试它而不是 Django 给你的任何验证。如果您在 ListViews 中更改查询集(自定义过滤等)或在 DetailViews 中更改 get_object() ,您应该检查结果列表和 404 错误是否根据您的自定义代码发生。
如果可以的话,我想提出一个更好的执行此测试的方法,即使用 the built-in Django test client。这将为您处理所有 CSRF 检查,并且更易于使用。它看起来像这样:
def test_home_page_returns_correct_html_with_POST(self):
url = reverse('your_home_page_view_url_name')
response = self.client.post(url, {'item_text': 'A new list item'})
self.assertContains(response, 'A new list item')
请注意,这也使用了 assertContains
,这是一个断言 provided by the Django test suite。
从您提供的代码片段来看,您似乎正在研究 "Test Driven Development with Python" 书中的示例,但并未使用 Django 1.8。
这本书的 Google 小组讨论中的 post 解决了您遇到的测试失败问题:
https://groups.google.com/forum/#!topic/obey-the-testing-goat-book/fwY7ifEWKMU/discussion
这个 GitHub 问题(来自本书的官方存储库)描述了与您的问题一致的修复:
我 运行 也遇到了这个问题(根据本书的第 2 版使用最新的 python 3.6.12 和 django 1.11.29)。
我的解决方案没有回答您的问题 'how do I render the token',但确实回答了 'how do I pass the test that compares the rendered template to the returned view response'。
我使用了以下代码:
class HomePageTest(TestCase):
def remove_csrf_tag(self, text):
'''Remove csrf tag from text'''
return re.sub(r'<[^>]*csrfmiddlewaretoken[^>]*>', '', text)
def test_home_page_is_about_todo_lists(self):
# Make an HTTP request
request = HttpRequest()
# Call home page view function
response = home_page(request)
# Assess if response contains the HTML we're looking for
# First read and open the template file ..
expected_content = render_to_string('lists/home.html', request=request)
print(len(response.content.decode()))
# .. then check if response is equal to template file
# (note that response is in bytecode, hence decode() method)
self.assertEqual(
self.remove_csrf_tag(response.content.decode()),
self.remove_csrf_tag(expected_content),
)
PS:我基于 this answer.
我有类似的问题所以做了一个函数来删除所有的 csrf 标记。
def test_home_page_returns_correct_html(self):
request = HttpRequest()
# Removes all the csrf token strings
def rem_csrf_token(string):
# Will contain everything before the token
startStr = ''
# Will contain everything after the token
endStr = ''
# Will carrry the final output
finalStr = string
# The approach is to keep finding the csrf token and remove it from the final string until there is no
# more token left and the str.index() method raises value arror
try:
while True:
# The beginning of the csrf token
ind = finalStr.index('<input type="hidden" name="csrfmiddlewaretoken"')
# The token end index
ind2 = finalStr.index('">', ind, finalStr.index('</form>'))
# Slicing the start and end string
startStr = finalStr[:ind]
endStr = finalStr[ind2+2:]
# Saving the final value (after removing one csrf token) and looping again
finalStr = startStr +endStr
except ValueError:
# It will only be returned after all the tokens have been removed :)
return finalStr
response = home_page(request)
expected_html = render_to_string('lists/home.html')
csrf_free_response = rem_csrf_token(response.content.decode())
self.assertEqual(csrf_free_response,
expected_html, f'{expected_html}\n{csrf_free_response}')
因此,在使用表单输入比较预期与实际 html 时,我一直在 Django 中返回失败测试,所以我打印出结果并意识到差异是相当简单的一行,由我的 {% csrf_token %}
,如下:
<input type='hidden' name='csrfmiddlewaretoken' value='hrPLKVOlhAIXmxcHI4XaFjqgEAMCTfUa' />
所以,我希望得到一个简单的答案,但我没能找到: 如何呈现 csrf_token 的结果以用于测试?
这是测试设置和失败:
def test_home_page_returns_correct_html_with_POST(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertIn('A new list item', response.content.decode())
expected_html = render_to_string(
'home.html',
{'new_item_text': 'A new list item'},
******this is where I'm hoping for a simple one-line mapping******
)
self.assertEqual(response.content.decode(), expected_html)
这是来自 views.py 的效果图:
def home_page(request):
return render(request, 'home.html', {
'new_item_text': request.POST.get('item_text'),
})
这是测试失败,当我 运行 使用 python manage.py test
FAIL: test_home_page_returns_correct_html_with_POST (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Me\PycharmProjects\superlists\lists\tests.py", line 29, in test_home_page_returns_correct_html_with_POST
self.assertEqual(response.content.decode(), expected_html)
AssertionError: '<!DO[298 chars] <input type=\'hidden\' name=\'csrfmiddlew[179 chars]tml>' != '<!DO[298 chars] \n </form>\n\n <table
id="id_list_t[82 chars]tml>'
----------------------------------------------------------------------
CSRF 令牌是模板上下文数据的一部分,如果您使用 Django TestCase 类:
response = self.client.get(url)
print(response.context)
https://docs.djangoproject.com/en/1.9/topics/testing/tools/#django.test.Response
关键是csrf_token
.
https://docs.djangoproject.com/en/1.9/_modules/django/template/context_processors/
编辑: 正如您所问如何将测试中呈现的 HTML 与测试服务器的输出进行比较:
因为您在模板中使用 {% csrf_token %}
,所以您无法将响应上下文中的 CSRF 令牌提供给 render_to_string
方法以使其使用相同的值。相反,您必须在 render_to_string
的结果中替换它,也许首先使用 selenium 查找输入元素(使其本身成为测试)。然而,这个测试有多大用处值得怀疑。它只会帮助确保存在 CSRF 令牌,但无论如何它已经在服务器上以正常工作模式进行了检查。
基本上,您应该测试您在代码中直接影响的任何内容,而不是 Django 魔法提供的任何内容。例如。如果你正在做自定义表单验证,你应该测试它而不是 Django 给你的任何验证。如果您在 ListViews 中更改查询集(自定义过滤等)或在 DetailViews 中更改 get_object() ,您应该检查结果列表和 404 错误是否根据您的自定义代码发生。
如果可以的话,我想提出一个更好的执行此测试的方法,即使用 the built-in Django test client。这将为您处理所有 CSRF 检查,并且更易于使用。它看起来像这样:
def test_home_page_returns_correct_html_with_POST(self):
url = reverse('your_home_page_view_url_name')
response = self.client.post(url, {'item_text': 'A new list item'})
self.assertContains(response, 'A new list item')
请注意,这也使用了 assertContains
,这是一个断言 provided by the Django test suite。
从您提供的代码片段来看,您似乎正在研究 "Test Driven Development with Python" 书中的示例,但并未使用 Django 1.8。
这本书的 Google 小组讨论中的 post 解决了您遇到的测试失败问题:
https://groups.google.com/forum/#!topic/obey-the-testing-goat-book/fwY7ifEWKMU/discussion
这个 GitHub 问题(来自本书的官方存储库)描述了与您的问题一致的修复:
我 运行 也遇到了这个问题(根据本书的第 2 版使用最新的 python 3.6.12 和 django 1.11.29)。
我的解决方案没有回答您的问题 'how do I render the token',但确实回答了 'how do I pass the test that compares the rendered template to the returned view response'。
我使用了以下代码:
class HomePageTest(TestCase):
def remove_csrf_tag(self, text):
'''Remove csrf tag from text'''
return re.sub(r'<[^>]*csrfmiddlewaretoken[^>]*>', '', text)
def test_home_page_is_about_todo_lists(self):
# Make an HTTP request
request = HttpRequest()
# Call home page view function
response = home_page(request)
# Assess if response contains the HTML we're looking for
# First read and open the template file ..
expected_content = render_to_string('lists/home.html', request=request)
print(len(response.content.decode()))
# .. then check if response is equal to template file
# (note that response is in bytecode, hence decode() method)
self.assertEqual(
self.remove_csrf_tag(response.content.decode()),
self.remove_csrf_tag(expected_content),
)
PS:我基于 this answer.
我有类似的问题所以做了一个函数来删除所有的 csrf 标记。
def test_home_page_returns_correct_html(self):
request = HttpRequest()
# Removes all the csrf token strings
def rem_csrf_token(string):
# Will contain everything before the token
startStr = ''
# Will contain everything after the token
endStr = ''
# Will carrry the final output
finalStr = string
# The approach is to keep finding the csrf token and remove it from the final string until there is no
# more token left and the str.index() method raises value arror
try:
while True:
# The beginning of the csrf token
ind = finalStr.index('<input type="hidden" name="csrfmiddlewaretoken"')
# The token end index
ind2 = finalStr.index('">', ind, finalStr.index('</form>'))
# Slicing the start and end string
startStr = finalStr[:ind]
endStr = finalStr[ind2+2:]
# Saving the final value (after removing one csrf token) and looping again
finalStr = startStr +endStr
except ValueError:
# It will only be returned after all the tokens have been removed :)
return finalStr
response = home_page(request)
expected_html = render_to_string('lists/home.html')
csrf_free_response = rem_csrf_token(response.content.decode())
self.assertEqual(csrf_free_response,
expected_html, f'{expected_html}\n{csrf_free_response}')