用Django进行测试驱动开发 第3部分
在Django中进行测试驱动开发,第3部分
这是一个用于理解Django测试驱动开发(Test Driven Development,以下简称TDD)的学习笔记。
我将参考《Python的测试驱动开发:遵从测试山羊:使用Django、Selenium和JavaScript(英文版)第二版》这本书去继续学习。
在本书中,我们使用Django1.1系列和FireFox进行功能测试等,但本次将使用Djagno3系列和Google Chrome进行功能测试。另外,我们进行了一些个人改动,例如将项目名称改为Config,但没有进行大的变更。
=>=>请点击此处查看第1章。
=>=>请点击此处查看第2章。
第一部分: TDD 和 Django 的基础知识
第三章。用单元测试测试简单主页。
在第二章,我们使用unittest.TestCase来编写功能测试,并测试页面标题是否包含”To-Do”。
现在,我们要实际启动应用程序并进行TDD。
我们的第一个Django应用程序,以及我们的第一个单元测试
Django 让我们可以在一个项目下构建多个应用程序。现在,让我们立刻开始创建 Django 应用程序。这里我们将创建一个名为 lists 的应用程序。
$ python manage.py startapp lists
单元测试和功能测试的区别
機能测试是从外部(用户的角度)来测试应用程序是否正常工作,而单元测试则是从内部(开发者的角度)测试应用程序是否正常工作。
TDD要求覆盖功能测试和单元测试,并且开发步骤如下所示。
步骤1:撰写功能测试(从用户角度描述新功能)。
第二步骤:当功能测试失败时,思考如何编写代码才能让测试通过(不要直接写)。通过添加单元测试来定义所期望代码的行为方式。
第三步:如果单元测试失败,则编写能够通过单元测试的最小应用程序代码。
第四步:重复步骤二和步骤三,最后确认功能测试是否通过。
在Django中进行单元测试
我们会将首页视图的测试写在 lists/tests.py 中。首先请确认一下这里。
# lists/tests.py
from django.test import TestCase
# Create your tests here.
通过查看这个,了解到了可以使用Django提供的TestCase类来编写单元测试。django.test.TestCase是扩展了功能测试中使用的标准模块unittest.TestCase的包装类。我们可以尝试编写一个单元测试来验证。
# lists/tests.py
from django.test import TestCase
class SmokeTest(TestCase):
def test_bad_maths(self):
self.assertEqual(1 + 1, 3)
让我们启动Django的测试运行器,它可以查找并执行每个应用程序的测试。
$ 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 "C:\--your_pass--\django-TDD\lists\tests.py", line 9, in test_bad_maths
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 1 test in 0.005s
FAILED (failures=1)
Destroying test database for alias 'default'...
我确认执行了lists/tests.py,并且出现了失败。我会在这里提交。
$ git status
$ git add lists
$ git commit -m "Add app for lists, with deliberately failing unit test"
Django的MVC架构,URL以及视图函数
在 Django 中,需要定义 Django 应该对特定的 URL 执行什么操作。Django 的工作流程如下述。
-
- 当HTTP请求发送到特定的URL时,
-
- 根据规则确定应该执行哪个视图来处理HTTP请求,
- 视图会处理请求并返回HTTP响应。
因此,我们需要做的有以下两点。
-
- 能否解决URL和视图的关联问题(解决URL的映射关系)
- 视图是否能够更改能通过功能测试的HTML
那么我们打开 lists/tests.py 文件,并写一些小的测试案例试试看。
# lists/tests.py
from django.urls import resolve # 追加
from django.test import TestCase
from lists.views import home_page # 追加
class HomePageTest(TestCase):
def test_root_url_resolve_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
django.urls.resolve是Django用于解析内部URL的模块。
from lists.views import home_page是即将编写的视图。
我们将在下一步进行测试。
$ python manage.py test
System check identified no issues (0 silenced).
E
======================================================================
ERROR: lists.tests (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: lists.tests
Traceback (most recent call last):
File "C:\--your_user_name--\AppData\Local\Programs\Python\Python37\lib\unittest\loader.py", line 436, in _find_test_path
module = self._get_module_from_name(name)
File "C:\--your_user_name--\AppData\Local\Programs\Python\Python37\lib\unittest\loader.py", line 377, in _get_module_from_name
__import__(name)
File "C:\--your_path--\django-TDD\lists\tests.py", line 5, in <module>
from lists.views import home_page
ImportError: cannot import name 'home_page' from 'lists.views' (C:\Users\--your_path--\django-TDD\lists\views.py)
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
出现了ImportError。查看其内容可知,它告诉我们无法从lists.views中导入home_page。
那么让我们尝试在lists.views中编写home_page吧。
# lists/views.py
from django.shortcuts import render
home_page = None
这可能听起来像是一个玩笑,但是这应该能解决ImportError的问题。TDD是写出解决错误的最小代码的过程,请回想起这种心情。
我将再次尝试进行测试。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_root_url_resolve_to_home_page_view (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 10, in test_root_url_resolve_to_home_page_view
found = resolve('/')
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\django\urls\base.py", line 25, in resolve
return get_resolver(urlconf).resolve(path)
File "C:\--your_path--\django-TDD\venv-tdd\lib\site-packages\django\urls\resolvers.py", line 575, in resolve
raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <URLPattern list> (admin:admin) 'admin/'>]], 'path': ''}
----------------------------------------------------------------------
Ran 1 test in 0.005s
FAILED (errors=1)
Destroying test database for alias 'default'...
虽然ImportError问题已经解决了,但测试又失败了。仔细查看Traceback,我们发现即使resolve函数解决了’/’路径,Django仍然返回404错误。也就是说,Django不能解析’/’路径的意思。
urls.py 的中文释义是“网址.py”。
Django中存在着将URL与视图进行映射的urls.py文件。config/urls.py是主要的urls.py文件,请查看此文件。
# config/urls.py
"""config URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
当我确认了内容后,我会参考写有“config URL Configuration”的URL映射方式。这次我们将尝试使用函数视图的方式,将”/”和”home_page”的映射添加到urlpatterns中。另外,由于管理员界面(admin/)还没有使用,我会将其注释掉。
# config/urls.py
from django.contrib import admin
from django.urls import path
from lists import views
urlpatterns = [
# path('admin/', admin.site.urls), # コメントアウト
path('', views.home_page, name='home')
]
我们进行了映射。我们要进行测试。
$ python manage.py test
[...]
TypeError: view must be a callable or a list/tuple in the case of include().
由于添加了URL映射,解决了404错误,但是出现了TypeError错误。
我认为这是因为当从lists.view调用home_page时,home_page = None没有返回任何内容。
让我们编辑lists/views.py来解决这个问题。
# lists/views.py
from django.shortcuts import render
def home_page():
pass
我会进行测试。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.003s
OK
Destroying test database for alias 'default'...
我已通过单体测试,现在我将提交它。
$ git add .
$ git status
$ git commit -m "First unit test and url mapping, dummy view"
为了测试当前的lists/views.py是否实际返回了HTML,我们将对lists/tests.py进行修改。
# lists/tests.py
from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest
from lists.views import home_page
class HomePageTest(TestCase):
def test_root_url_resolve_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
def test_home_page_returns_current_html(self): # 追加
request = HttpRequest()
response = home_page(request)
html = response.content.decode('utf8')
self.assertTrue(html.startswith.('<html>'))
self.assertIn('<title>To-Do lists</title>', html)
self.assertTrue(html.endwith('</html>'))
测试test_root_url_resolve_to_home_page_view是否能正确地进行URL映射,
测试test_home_page_returns_current_html是否能正确地返回HTML。
由于添加了新的单元测试,让我们立即进行测试。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E.
======================================================================
ERROR: test_home_page_returns_current_html (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 18, in test_home_page_returns_current_html
response = home_page(request)
TypeError: home_page() takes 0 positional arguments but 1 was given
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (errors=1)
Destroying test database for alias 'default'...
出现了 TypeError。根据确认的内容,home_page() 的定义中没有指定参数(0个位置参数),但给出了一个参数(1个参数)是不正确的。
所以,我想要修改 lists/views.py 这个文件。
# lists/views.py
from django.shortcuts import render
def home_page(request): # 変更
pass
我在home_page()函数中添加了参数request。现在我来进行测试。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E.
======================================================================
ERROR: test_home_page_returns_current_html (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 19, in test_home_page_returns_current_html
html = response.content.decode('utf8')
AttributeError: 'NoneType' object has no attribute 'content'
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (errors=1)
Destroying test database for alias 'default'...
TypeError已经解决,但接下来出现了AttributeError。
由于出现了’NoneType’ object has no attribute ‘content’的错误,可以推断home_page(request)的返回值为None是原因。
我们需要修正lists/views.py。
# lists/views.py
from django.shortcuts import render
from django.http import HttpResponse # 追加
def home_page(request):
return HttpResponse()
我已经修正为返回django.http.HttpResponse。现在让我们进行测试。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_home_page_returns_current_html (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\--your_path--\django-TDD\lists\tests.py", line 20, in test_home_page_returns_current_html
self.assertTrue(html.startswith('<html>'))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (failures=1)
Destroying test database for alias 'default'...
如下所示的 AttributeError 已經被處理,但是發生了 AssertionError。由於 html.startwith(”) 是 False,所以出現了 “False is not ture” 的訊息。我們將修正 lists/views.py。
# lists/views.py
from django.shortcuts import render
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html>') # 変更
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_home_page_returns_current_html (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 21, in test_home_page_returns_current_html
self.assertIn('<title>To-Do lists</title>', html)
AssertionError: '<title>To-Do lists</title>' not found in '<html>'
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (failures=1)
Destroying test database for alias 'default'...
同样的是AssertionError。发现无法找到”。
将修正lists/views.py文件。
# lists/views.py
from django.shortcuts import render
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title>') # 変更
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_home_page_returns_current_html (lists.tests.SmokeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\--your_path--\django-TDD\lists\tests.py", line 22, in test_home_page_returns_current_html
self.assertTrue(html.endswith('</html>'))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.013s
FAILED (failures=1)
Destroying test database for alias 'default'...
同样是AssertionError。找不到”。
将修正lists/views.py。
# lists/views.py
from django.shortcuts import render
from django.http import HttpResponse
def home_page(request):
return HttpResponse('<html><title>To-Do lists</title></html>') # 変更
希望这次终于能够成功了。
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s
OK
Destroying test database for alias 'default'...
我们成功了。由于通过了单元测试,我们可以启动开发服务器并进行功能测试。
# 開発サーバーの立ち上げ
$ python manage.py runserver
# 別のcmdを立ち上げて機能テストを実行
$ python functional_tests.py
DevTools listening on ws://127.0.0.1:51108/devtools/browser/9d1c6c55-8391-491b-9b14-6130c3314bba
F
======================================================================
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 7.244s
FAILED (failures=1)
機能测试的结果显示为FAILED,但这是因为我们使用了unittest.TestCase的.fail来测试,并确保始终会发生错误。
因此,我们可以知道功能测试成功了!
我们来提交吧。
$ git add .
$ git commit -m "Basic view now return minimal HTML"
第三章总结
我要确认一下我们到目前为止所涵盖的内容。
-
- Djagnoのアプリケーションをスタートしました。
-
- Djangoのユニットテストランナーを使いました。
-
- 機能テストと単体テストの違いを理解しました。
-
- Djangoのrequestとresponseオブジェクトを使ってviewを作成しました
- 基本的なHTMLを返しました。
可以通过进行单元测试和代码的迭代修正循环来确认功能的创建过程。