单元测试

Useful Library for Testing Django Projects coverage, 整合测试的有效工具

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

pytest-django django-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')


Buy me a 肥仔水!