Useful Library for Testing Django Projects
coverage
, 整合测试的有效工具
怎么样结构化
python manage.py startapp appdemo
会生成一个 tests.py
文件
把它替换成一个 package
appdemo/
__init__.py
admin.py
forms.py
models.py
tests/
__init__.py
test_forms.py
test_models.py
test_views.py
views.py
怎么实现单元测试
write the most meaningful tests in the shortest amount of time
- 每个测试方法只测试一个件事情
Each Test Method Tests One Thing
# flavors/tests/test_api.py
import json
from django.test import TestCase
from django.urls import reverse
from flavors.models import Flavor
class FlavorAPITests(TestCase):
def setUp(self):
Flavor.objects.get_or_create(title='A Title', slug='a-slug')
def test_list(self):
url = reverse('flavor_object_api')
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
data = json.loads(response.content)
self.assertEquals(len(data), 1)
更完整的例子
# flavors/tests/test_api.py
import json
from django.test import TestCase
from django.urls import reverse
from flavors.models import Flavor
class DjangoRestFrameworkTests(TestCase):
def setUp(self):
Flavor.objects.get_or_create(title='title1', slug='slug1')
Flavor.objects.get_or_create(title='title2', slug='slug2')
self.create_read_url = reverse('flavor_rest_api')
self.read_update_delete_url = \
reverse('flavor_rest_api', kwargs={'slug': 'slug1'})
def test_list(self):
response = self.client.get(self.create_read_url)
# Are both titles in the content?
self.assertContains(response, 'title1')
self.assertContains(response, 'title2')
def test_detail(self):
response = self.client.get(self.read_update_delete_url)
data = json.loads(response.content)
content = {'id': 1, 'title': 'title1', 'slug': 'slug1',
'scoops_remaining': 0}
self.assertEquals(data, content)
def test_create(self):
post = {'title': 'title3', 'slug': 'slug3'}
response = self.client.post(self.create_read_url, post)
data = json.loads(response.content)
self.assertEquals(response.status_code, 201)
content = {'id': 3, 'title': 'title3', 'slug': 'slug3',
'scoops_remaining': 0}
self.assertEquals(data, content)
self.assertEquals(Flavor.objects.count(), 3)
def test_delete(self):
response = self.client.delete(self.read_update_delete_url)
self.assertEquals(response.status_code, 204)
self.assertEquals(Flavor.objects.count(), 1)
- 使用
django.test.client.RequestFactory
生成request 对象
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from .views import cheese_flavors
def add_middleware_to_request(request, middleware_class):
middleware = middleware_class()
middleware.process_request(request)
return request
def add_middleware_to_response(request, middleware_class):
middleware = middleware_class()
middleware.process_response(request)
return request
class SavoryIceCreamTest(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
def test_cheese_flavors(self):
request = self.factory.get('/cheesy/broccoli/')
request.user = AnonymousUser()
# Annotate the request object with a session
request = add_middleware_to_request(request, SessionMiddleware)
request.session.save()
# process and test the request
response = cheese_flavors(request)
self.assertContains(response, 'bleah!')
- Use
Mock
to Keep Unit Tests From Touching the World
# Using Mock to Keep Unit Tests From Touching the World
from unittest import mock, TestCase
import icecreamapi
from flavors.exceptions import CantListFlavors
from flavors.utils import list_flavors_sorted
class TestIceCreamSorting(TestCase):
# Set up monkeypatch of icecreamapi.get_flavors()
@mock.patch.object(icecreamapi, 'get_flavors')
def test_flavor_sort(self, get_flavors):
# Instructs icecreamapi.get_flavors() to return an unordered list.
get_flavors.return_value = ['chocolate', 'vanilla', 'strawberry', ]
# list_flavors_sorted() calls the icecreamapi.get_flavors()
# function. Since we've monkeypatched the function, it will always
# return ['chocolate', 'strawberry', 'vanilla', ]. Which the.
# list_flavors_sorted() will sort alphabetically
flavors = list_flavors_sorted()
self.assertEqual(
flavors,
['chocolate', 'strawberry', 'vanilla', ]
)
# Testing For When API is Unavailable
@mock.patch.object(icecreamapi, 'get_flavors')
def test_flavor_sort_failure(self, get_flavors):
# Instructs icecreamapi.get_flavors() to throw a FlavorError.
get_flavors.side_effect = icecreamapi.FlavorError()
# list_flavors_sorted() catches the icecreamapi.FlavorError()
# and passes on a CantListFlavors exception.
with self.assertRaises(CantListFlavors):
list_flavors_sorted()
# Testing python-requests Connection Failures
@mock.patch.object(requests, 'get')
def test_request_failure(self, get):
"""Test if the target site is innaccessible."""
get.side_effect = requests.exception.ConnectionError()
with self.assertRaises(CantListFlavors):
list_flavors_sorted()
@mock.patch.object(requests, 'get')
def test_request_failure(self, get)
"""Test if we can handle SSL problems elegantly."""
get.side_effect = requests.exception.SSLError()
with self.assertRaises(CantListFlavors):
list_flavors_sorted()
#
-
使用
assert method
➤ assertRaises ➤ Python 2.7: ListItemsEqual(), Python 3+ assertCountEqual() ➤ assertDictEqual() ➤ assertFormError() ➤ assertContains() Check status 200, checks in response.content. ➤ assertHTMLEqual() Amongst many things, ignores whitespace differences. ➤ assertJSONEqual()
-
给每个测试做好注释说明
单元测试完成之后的整合测试(Integration Tests
) 使用coverage
通常的步骤
➤ Selenium tests to confirm that an application works in the browser.
➤ Actual testing against a third-party API instead of mocking responses. For example, Django
Packages conducts periodic tests against GitHub and the PyPI API to ensure that its interaction with those systems is valid.
➤ Interacting with requestb.in or httpbin.org to confirm the validity of outbound requests.
➤ Using runscope.com to validate that our API is working as expected.
整合测试的缺点
➤ Setting up integration tests can take a lot of time.
➤ Compared to unit tests, integrations are extremely slow. That’s because instead of testing the
smallest components, integration tests are, by definition, testing the whole system.
➤ When errors are thrown by integration tests, uncovering the problem is harder than unit tests.
For example, a problem affecting a single type of browser might be caused by a unicode transformation happening at the database level.
➤ Integration tests are fragile compared to unit tests. A small change in a component or setting
can break them. We’ve yet to work on a significant project where at least one person wasn’t
forever blocked from running them successfully.
- 使用
coverage
1 完成单元测试
2 运行测试
# Running Django tests using coverage.py
coverage run manage.py test --settings=twoscoops.settings.test
结果
Creating test database for alias "default"...
..
-----------------------------------------------
Ran 2 tests in 0.008s
OK
Destroying test database for alias "default"...
3 生成报告 HTML
provide percentage numbers of what’s been covered by tests, it also shows us the places where code is not
tested.
# Test Results Without admin.py
$ coverage html --omit="admin.py"
替代 unittest
的其他方式
pytest
nose
# test_models.py
from pytest import raises
from cones.models import Cone
def test_good_choice():
assert Cone.objects.filter(type='sugar').count() == 1
def test_bad_cone_choice():
with raises(Cone.DoesNotExist):
Cone.objects.get(type='spaghetti')