python-tdd-book/textbook/chap3.md

225 lines
8.2 KiB
Markdown
Raw Normal View History

2020-08-19 15:50:58 +10:00
# Chapter 3. Testing a Simple Home Page with Unit Tests
Chap2 introduced writing a functional test using unittest module (expected failure), now create a working To-Do list app
## 3.1 Our First Django App, and Our First Unit Test
* Django encourage structure the code into apps. We can reuse app developed by us or others
2020-11-06 12:34:46 +11:00
Create an app `lists` (sub-directory) via `python manage.py startapp lists` in the directory which contains `functional_tests.py` & `manage.py`, this subdir should include `tests.py` be default
2020-08-19 15:50:58 +10:00
## 3.2 Unit Tests, and How They Differ from Functional Tests
Unit Tests vs Functional Tests:
* **Unit Tests (UT)**: tests app from inside as programmer
* **Functional Tests (FT)**: test app from outside as user
TDD approach will cover both, with following workflow:
1. Writing FT that describing new functionality
2. After expected failure of FT, design app that can get it pass FT. And write UTs that define how code behave. Benchmark, each production code we write should be tested by at least 1 of our unit tests.
3. After having a failure UT, write the smallest amount of app code to pass the UT.
4. Iterate step 2 & 3 to the point that we can test with FT
5. Rerun FT and it should pass.
6. Keep writing new UT & FT
Summary:
* FT evaluate app on high level
* UT drive development on low level
## 3.3 Unit Testing in Django
1. `lists/tests.py` provide place to write UT/FT. Adding following to import Django's `TestCase` which is inherited from `unittest`
```python
from django.test import TestCase
# Create tests from here
```
2. Before writing any new FT/UT, make sure UT can be run by automated test runner, as `lists/tests.py` is created by unittest. while previously we directly call `python functional_tests.py`. Running Django's test framework via:
```python
python manage.py test
```
## 3.4 Django's MVC, URLs, and View Functions
2020-11-06 12:34:46 +11:00
Django is structured along classic [**Model-View-Controller (MVC)** pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
2020-08-19 15:50:58 +10:00
Django's main job is to decide what to do when a user asks for a particular URL on our site.
2020-08-19 15:50:58 +10:00
2020-11-06 12:34:46 +11:00
Django's workflow:
2020-08-19 15:50:58 +10:00
1. An HTTP `request` comes in for a particular URL
2. *Resolving the URL*: Django uses some rules to decide which `view` function should deal with the request.
3. The view function processes the request and returns an HTTP `response`
2020-08-19 15:50:58 +10:00
Let's design a tests:
1. Verify we can resolve the URL for the root of site ("/") to a particular view function we've made?
2. Verify view function return some HTML which will get the functional test to pass?
2020-11-06 12:34:46 +11:00
```python
from django.test import TestCase
from django.urls import resolve
from lists.views import home_page
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
```
Where:
* The resolve() function if `django.url` package can be used for resolving URL paths to the corresponding view functions. The function returns a [`ResolverMatch` object](https://docs.djangoproject.com/en/3.1/ref/urlresolvers/#django.urls.ResolverMatch) that allows you to access various metadata about the resolved URL.
* In `assertEqual(...)`, we try to test that `found.func` is `home_page` (a view function we haven't defined yet)
Result of running test:
```
ImportError: cannot import name 'home_page'
```
## 3.5 At Last! We Actually Write Some Application Code!
2020-11-06 12:34:46 +11:00
In 3.4, we created a `test_root_url_resolves_to_home_page_view()` test case that check "django can resolve request to root and return a view function"
Hence, add content in `lists/views.py`, we can pass the test
```python
from django.shortcuts import render
home_page = None
```
2020-11-06 12:34:46 +11:00
Running the test generate following info
```
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/jason/HomeWorkstation/SynologyGiteaSpace/python-tdd-book-src/src/lists/tests.py", line 8, in test_root_url_resolves_to_home_page_view
found = resolve('/')
File "/home/jason/miniconda3/envs/python-tdd-book/lib/python3.6/site-packages/django/urls/base.py", line 27, in resolve
return get_resolver(urlconf).resolve(path)
File "/home/jason/miniconda3/envs/python-tdd-book/lib/python3.6/site-packages/django/urls/resolvers.py", line 394, in resolve
raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''}
----------------------------------------------------------------------
Ran 1 test in 0.001s
```
* error due to `django.urls.exceptions.Resolver404: {'tried': [[<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''}`
* Happen in `python-tdd-book-src/src/lists/tests.py", line 8, in test_root_url_resolves_to_home_page_view`
Overall, the traceback can be interpreted as: "when trying to resolve `/`, Django raised a 404 error". i.e. Django can't find a URL mapping for "/"
## 3.6 urls.py
2020-11-07 15:27:12 +11:00
Django use `urls.py` in each app to map URLs to view functions.
2020-11-07 15:27:12 +11:00
```python
urlpatterns = [
url(r'^admin/', admin.site.urls),
]
```
Note:
* This book use Django v1.11. So `path` function is not utilized, it's used in v3.x
Explain:
* `url` start with a regex, which defines which URLs it looks for, and where (function) should these request to send
* `^$` means empty string
Modifying `superlists.urls` to
```python
from django.conf.urls import url
from lists import views
urlpatterns = [
url(r'^$', views.home_page, name='home'),
]
```
Generate error as shown
```
...
File "/home/jason/HomeWorkstation/SynologyGiteaSpace/python-tdd-book-src/src/superlists/urls.py", line 20, in <module>
url(r'^$', views.home_page, name='home'),
File "/home/jason/miniconda3/envs/python-tdd-book/lib/python3.6/site-packages/django/conf/urls/__init__.py", line 85, in url
raise TypeError('view must be a callable or a list/tuple in the case of include().')
TypeError: view must be a callable or a list/tuple in the case of include().
```
Analyzing error message "TypeError: view must be a callable or a list/tuple in the case of include().": unit tests have actually made the link btw the URL "/" and `home_page = None` in `lists/views.py`, and are now complaining that `home_page` view is not callable.
Fix this problem via modifying `lists/views.py`
```python
def home_page():
pass
```
## 3.7 Unit Testing a View
2020-11-07 15:27:12 +11:00
After creating simple `home_page` empty function. We can create test function in `lists/tests.py`
```python
2020-11-07 21:36:51 +11:00
def test_home_page_returns_correct_html(self):
"""
1. Create an HttpRequest object, which is what Django will see when a user's browser asks for a page
2. Pass it to `home_page` view, which gives us a response.
3. Extract `.content` of the response, which are byte value, and should be decoded into string (HTML format)
4. Check HTML starts and end with <html> tag
5. Want to find <title> tag in the middle
"""
request = HttpRequest() #1
response = home_page(request) #2
html = response.content.decode('utf8') #3
self.assertTrue(html.startswith('<html>')) #4
self.assertIn('<title>To-Do lists</title>', html.strip()) #5
self.assertTrue(html.strip().endswith('</html>')) #4
2020-11-07 15:27:12 +11:00
```
Result of `python manager.py test` is:
```
line 15, in test_home_page_returns_correct_html
response = home_page(request)
TypeError: home_page() takes 0 positional arguments but 1 was given
```
### 3.7.1 The Unit-Test/Code Cycle
TDD *unit-test/code cycle*:
1. In the terminal, run unit tests and see how they fail
2. In the editor, create minimal code change to fix test failure.
3. Repeat
Fix the `views.py` to
```python
def home_page(request):
return HttpResponse('<html><title>To-Do list</title></html>')
```
Conclusion of covering:
* Starting a Django app
* The Django unit test runner
* The difference btw FTs and unit tests
* Django URL resolving and `urls.py`
* Django view functions, request and response objects
* And returning basic HTML