Django assert error

The web framework for perfectionists with deadlines.

Testing tools¶

Django provides a small set of tools that come in handy when writing tests.

The test client¶

The test client is a Python class that acts as a dummy web browser, allowing
you to test your views and interact with your Django-powered application
programmatically.

Some of the things you can do with the test client are:

  • Simulate GET and POST requests on a URL and observe the response –
    everything from low-level HTTP (result headers and status codes) to
    page content.
  • See the chain of redirects (if any) and check the URL and status code at
    each step.
  • Test that a given request is rendered by a given Django template, with
    a template context that contains certain values.

Note that the test client is not intended to be a replacement for Selenium or
other “in-browser” frameworks. Django’s test client has a different focus. In
short:

  • Use Django’s test client to establish that the correct template is being
    rendered and that the template is passed the correct context data.
  • Use RequestFactory to test view functions directly,
    bypassing the routing and middleware layers.
  • Use in-browser frameworks like Selenium to test rendered HTML and the
    behavior of web pages, namely JavaScript functionality. Django also
    provides special support for those frameworks; see the section on
    LiveServerTestCase for more details.

A comprehensive test suite should use a combination of all of these test types.

Overview and a quick example¶

To use the test client, instantiate django.test.Client and retrieve
web pages:

>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'

As this example suggests, you can instantiate Client from within a session
of the Python interactive interpreter.

Note a few important things about how the test client works:

  • The test client does not require the web server to be running. In fact,
    it will run just fine with no web server running at all! That’s because
    it avoids the overhead of HTTP and deals directly with the Django
    framework. This helps make the unit tests run quickly.

  • When retrieving pages, remember to specify the path of the URL, not the
    whole domain. For example, this is correct:

    This is incorrect:

    >>> c.get('https://www.example.com/login/')
    

    The test client is not capable of retrieving web pages that are not
    powered by your Django project. If you need to retrieve other web pages,
    use a Python standard library module such as urllib.

  • To resolve URLs, the test client uses whatever URLconf is pointed-to by
    your ROOT_URLCONF setting.

  • Although the above example would work in the Python interactive
    interpreter, some of the test client’s functionality, notably the
    template-related functionality, is only available while tests are
    running
    .

    The reason for this is that Django’s test runner performs a bit of black
    magic in order to determine which template was loaded by a given view.
    This black magic (essentially a patching of Django’s template system in
    memory) only happens during test running.

  • By default, the test client will disable any CSRF checks
    performed by your site.

    If, for some reason, you want the test client to perform CSRF
    checks, you can create an instance of the test client that
    enforces CSRF checks. To do this, pass in the
    enforce_csrf_checks argument when you construct your
    client:

    >>> from django.test import Client
    >>> csrf_client = Client(enforce_csrf_checks=True)
    

Making requests¶

Use the django.test.Client class to make requests.

class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, **defaults

It requires no arguments at time of construction. However, you can use
keyword arguments to specify some default headers. For example, this will
send a User-Agent HTTP header in each request:

>>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')

The values from the extra keyword arguments passed to
get(),
post(), etc. have precedence over
the defaults passed to the class constructor.

The enforce_csrf_checks argument can be used to test CSRF
protection (see above).

The raise_request_exception argument allows controlling whether or not
exceptions raised during the request should also be raised in the test.
Defaults to True.

The json_encoder argument allows setting a custom JSON encoder for
the JSON serialization that’s described in post().

Once you have a Client instance, you can call any of the following
methods:

get(path, data=None, follow=False, secure=False, **extra

Makes a GET request on the provided path and returns a Response
object, which is documented below.

The key-value pairs in the data dictionary are used to create a GET
data payload. For example:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7})

…will result in the evaluation of a GET request equivalent to:

/customers/details/?name=fred&age=7

The extra keyword arguments parameter can be used to specify
headers to be sent in the request. For example:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
...       HTTP_ACCEPT='application/json')

…will send the HTTP header HTTP_ACCEPT to the details view, which
is a good way to test code paths that use the
django.http.HttpRequest.accepts() method.

CGI specification

The headers sent via **extra should follow CGI specification.
For example, emulating a different “Host” header as sent in the
HTTP request from the browser to the server should be passed
as HTTP_HOST.

If you already have the GET arguments in URL-encoded form, you can
use that encoding instead of using the data argument. For example,
the previous GET request could also be posed as:

>>> c = Client()
>>> c.get('/customers/details/?name=fred&age=7')

If you provide a URL with both an encoded GET data and a data argument,
the data argument will take precedence.

If you set follow to True the client will follow any redirects
and a redirect_chain attribute will be set in the response object
containing tuples of the intermediate urls and status codes.

If you had a URL /redirect_me/ that redirected to /next/, that
redirected to /final/, this is what you’d see:

>>> response = c.get('/redirect_me/', follow=True)
>>> response.redirect_chain
[('http://testserver/next/', 302), ('http://testserver/final/', 302)]

If you set secure to True the client will emulate an HTTPS
request.

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra

Makes a POST request on the provided path and returns a
Response object, which is documented below.

The key-value pairs in the data dictionary are used to submit POST
data. For example:

>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})

…will result in the evaluation of a POST request to this URL:

…with this POST data:

If you provide content_type as application/json, the
data is serialized using json.dumps() if it’s a dict, list,
or tuple. Serialization is performed with
DjangoJSONEncoder by default,
and can be overridden by providing a json_encoder argument to
Client. This serialization also happens for put(),
patch(), and delete() requests.

If you provide any other content_type (e.g. text/xml
for an XML payload), the contents of data are sent as-is in the
POST request, using content_type in the HTTP Content-Type
header.

If you don’t provide a value for content_type, the values in
data will be transmitted with a content type of
multipart/form-data. In this case, the key-value pairs in
data will be encoded as a multipart message and used to create the
POST data payload.

To submit multiple values for a given key – for example, to specify
the selections for a <select multiple> – provide the values as a
list or tuple for the required key. For example, this value of data
would submit three selected values for the field named choices:

{'choices': ('a', 'b', 'd')}

Submitting files is a special case. To POST a file, you need only
provide the file field name as a key, and a file handle to the file you
wish to upload as a value. For example, if your form has fields
name and attachment, the latter a
FileField:

>>> c = Client()
>>> with open('wishlist.doc', 'rb') as fp:
...     c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

You may also provide any file-like object (e.g., StringIO or
BytesIO) as a file handle. If you’re uploading to an
ImageField, the object needs a name
attribute that passes the
validate_image_file_extension validator.
For example:

>>> from io import BytesIO
>>> img = BytesIO(
...     b"GIF89ax01x00x01x00x00x00x00!xf9x04x01x00x00x00"
...     b"x00,x00x00x00x00x01x00x01x00x00x02x01x00x00"
... )
>>> img.name = "myimage.gif"

Note that if you wish to use the same file handle for multiple
post() calls then you will need to manually reset the file
pointer between posts. The easiest way to do this is to
manually close the file after it has been provided to
post(), as demonstrated above.

You should also ensure that the file is opened in a way that
allows the data to be read. If your file contains binary data
such as an image, this means you will need to open the file in
rb (read binary) mode.

The extra argument acts the same as for Client.get().

If the URL you request with a POST contains encoded parameters, these
parameters will be made available in the request.GET data. For example,
if you were to make the request:

>>> c.post('/login/?visitor=true', {'name': 'fred', 'passwd': 'secret'})

… the view handling this request could interrogate request.POST
to retrieve the username and password, and could interrogate request.GET
to determine if the user was a visitor.

If you set follow to True the client will follow any redirects
and a redirect_chain attribute will be set in the response object
containing tuples of the intermediate urls and status codes.

If you set secure to True the client will emulate an HTTPS
request.

head(path, data=None, follow=False, secure=False, **extra

Makes a HEAD request on the provided path and returns a
Response object. This method works just like Client.get(),
including the follow, secure and extra arguments, except
it does not return a message body.

options(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra

Makes an OPTIONS request on the provided path and returns a
Response object. Useful for testing RESTful interfaces.

When data is provided, it is used as the request body, and
a Content-Type header is set to content_type.

The follow, secure and extra arguments act the same as for
Client.get().

put(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra

Makes a PUT request on the provided path and returns a
Response object. Useful for testing RESTful interfaces.

When data is provided, it is used as the request body, and
a Content-Type header is set to content_type.

The follow, secure and extra arguments act the same as for
Client.get().

patch(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra

Makes a PATCH request on the provided path and returns a
Response object. Useful for testing RESTful interfaces.

The follow, secure and extra arguments act the same as for
Client.get().

delete(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra

Makes a DELETE request on the provided path and returns a
Response object. Useful for testing RESTful interfaces.

When data is provided, it is used as the request body, and
a Content-Type header is set to content_type.

The follow, secure and extra arguments act the same as for
Client.get().

trace(path, follow=False, secure=False, **extra

Makes a TRACE request on the provided path and returns a
Response object. Useful for simulating diagnostic probes.

Unlike the other request methods, data is not provided as a keyword
parameter in order to comply with RFC 7231#section-4.3.8, which
mandates that TRACE requests must not have a body.

The follow, secure, and extra arguments act the same as for
Client.get().

login(**credentials

If your site uses Django’s authentication system
and you deal with logging in users, you can use the test client’s
login() method to simulate the effect of a user logging into the
site.

After you call this method, the test client will have all the cookies
and session data required to pass any login-based tests that may form
part of a view.

The format of the credentials argument depends on which
authentication backend you’re using
(which is configured by your AUTHENTICATION_BACKENDS
setting). If you’re using the standard authentication backend provided
by Django (ModelBackend), credentials should be the user’s
username and password, provided as keyword arguments:

>>> c = Client()
>>> c.login(username='fred', password='secret')

# Now you can access a view that's only available to logged-in users.

If you’re using a different authentication backend, this method may
require different credentials. It requires whichever credentials are
required by your backend’s authenticate() method.

login() returns True if it the credentials were accepted and
login was successful.

Finally, you’ll need to remember to create user accounts before you can
use this method. As we explained above, the test runner is executed
using a test database, which contains no users by default. As a result,
user accounts that are valid on your production site will not work
under test conditions. You’ll need to create users as part of the test
suite – either manually (using the Django model API) or with a test
fixture. Remember that if you want your test user to have a password,
you can’t set the user’s password by setting the password attribute
directly – you must use the
set_password() function to
store a correctly hashed password. Alternatively, you can use the
create_user() helper
method to create a new user with a correctly hashed password.

force_login(user, backend=None

If your site uses Django’s authentication
system
, you can use the force_login() method
to simulate the effect of a user logging into the site. Use this method
instead of login() when a test requires a user be logged in and
the details of how a user logged in aren’t important.

Unlike login(), this method skips the authentication and
verification steps: inactive users (is_active=False) are permitted to login
and the user’s credentials don’t need to be provided.

The user will have its backend attribute set to the value of the
backend argument (which should be a dotted Python path string), or
to settings.AUTHENTICATION_BACKENDS[0] if a value isn’t provided.
The authenticate() function called by
login() normally annotates the user like this.

This method is faster than login() since the expensive
password hashing algorithms are bypassed. Also, you can speed up
login() by using a weaker hasher while testing.

logout()¶

If your site uses Django’s authentication system,
the logout() method can be used to simulate the effect of a user
logging out of your site.

After you call this method, the test client will have all the cookies
and session data cleared to defaults. Subsequent requests will appear
to come from an AnonymousUser.

Testing responses¶

The get() and post() methods both return a Response object. This
Response object is not the same as the HttpResponse object returned
by Django views; the test response object has some additional data useful for
test code to verify.

Specifically, a Response object has the following attributes:

class Response
client

The test client that was used to make the request that resulted in the
response.

content

The body of the response, as a bytestring. This is the final page
content as rendered by the view, or any error message.

context

The template Context instance that was used to render the template that
produced the response content.

If the rendered page used multiple templates, then context will be a
list of Context objects, in the order in which they were rendered.

Regardless of the number of templates used during rendering, you can
retrieve context values using the [] operator. For example, the
context variable name could be retrieved using:

>>> response = client.get('/foo/')
>>> response.context['name']
'Arthur'

Not using Django templates?

This attribute is only populated when using the
DjangoTemplates backend.
If you’re using another template engine,
context_data
may be a suitable alternative on responses with that attribute.

exc_info

A tuple of three values that provides information about the unhandled
exception, if any, that occurred during the view.

The values are (type, value, traceback), the same as returned by
Python’s sys.exc_info(). Their meanings are:

  • type: The type of the exception.
  • value: The exception instance.
  • traceback: A traceback object which encapsulates the call stack at
    the point where the exception originally occurred.

If no exception occurred, then exc_info will be None.

json(**kwargs

The body of the response, parsed as JSON. Extra keyword arguments are
passed to json.loads(). For example:

>>> response = client.get('/foo/')
>>> response.json()['name']
'Arthur'

If the Content-Type header is not "application/json", then a
ValueError will be raised when trying to parse the response.

request

The request data that stimulated the response.

wsgi_request

The WSGIRequest instance generated by the test handler that
generated the response.

status_code

The HTTP status of the response, as an integer. For a full list
of defined codes, see the IANA status code registry.

templates

A list of Template instances used to render the final content, in
the order they were rendered. For each template in the list, use
template.name to get the template’s file name, if the template was
loaded from a file. (The name is a string such as
'admin/index.html'.)

Not using Django templates?

This attribute is only populated when using the
DjangoTemplates backend.
If you’re using another template engine,
template_name
may be a suitable alternative if you only need the name of the
template used for rendering.

resolver_match

An instance of ResolverMatch for the response.
You can use the func attribute, for
example, to verify the view that served the response:

# my_view here is a function based view.
self.assertEqual(response.resolver_match.func, my_view)

# Class-based views need to compare the view_class, as the
# functions generated by as_view() won't be equal.
self.assertIs(response.resolver_match.func.view_class, MyView)

If the given URL is not found, accessing this attribute will raise a
Resolver404 exception.

As with a normal response, you can also access the headers through
HttpResponse.headers. For example, you could determine the content
type of a response using response.headers['Content-Type'].

Exceptions¶

If you point the test client at a view that raises an exception and
Client.raise_request_exception is True, that exception will be visible
in the test case. You can then use a standard try ... except block or
assertRaises() to test for exceptions.

The only exceptions that are not visible to the test client are
Http404,
PermissionDenied, SystemExit, and
SuspiciousOperation. Django catches these
exceptions internally and converts them into the appropriate HTTP response
codes. In these cases, you can check response.status_code in your test.

If Client.raise_request_exception is False, the test client will return a
500 response as would be returned to a browser. The response has the attribute
exc_info to provide information about the unhandled
exception.

Persistent state¶

The test client is stateful. If a response returns a cookie, then that cookie
will be stored in the test client and sent with all subsequent get() and
post() requests.

Expiration policies for these cookies are not followed. If you want a cookie
to expire, either delete it manually or create a new Client instance (which
will effectively delete all cookies).

A test client has attributes that store persistent state information. You can
access these properties as part of a test condition.

Client.cookies

A Python SimpleCookie object, containing the current
values of all the client cookies. See the documentation of the
http.cookies module for more.

Client.session

A dictionary-like object containing session information. See the
session documentation for full details.

To modify the session and then save it, it must be stored in a variable
first (because a new SessionStore is created every time this property
is accessed):

def test_something(self):
    session = self.client.session
    session['somekey'] = 'test'
    session.save()

Setting the language¶

When testing applications that support internationalization and localization,
you might want to set the language for a test client request. The method for
doing so depends on whether or not the
LocaleMiddleware is enabled.

If the middleware is enabled, the language can be set by creating a cookie with
a name of LANGUAGE_COOKIE_NAME and a value of the language code:

from django.conf import settings

def test_language_using_cookie(self):
    self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'fr'})
    response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

or by including the Accept-Language HTTP header in the request:

def test_language_using_header(self):
    response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fr')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

Note

When using these methods, ensure to reset the active language at the end of
each test:

def tearDown(self):
    translation.activate(settings.LANGUAGE_CODE)

More details are in How Django discovers language preference.

If the middleware isn’t enabled, the active language may be set using
translation.override():

from django.utils import translation

def test_language_using_override(self):
    with translation.override('fr'):
        response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

More details are in Explicitly setting the active language.

Example¶

The following is a unit test using the test client:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def setUp(self):
        # Every test needs a client.
        self.client = Client()

    def test_details(self):
        # Issue a GET request.
        response = self.client.get('/customer/details/')

        # Check that the response is 200 OK.
        self.assertEqual(response.status_code, 200)

        # Check that the rendered context contains 5 customers.
        self.assertEqual(len(response.context['customers']), 5)

Provided test case classes¶

Normal Python unit test classes extend a base class of
unittest.TestCase. Django provides a few extensions of this base class:

Hierarchy of Django unit testing classes (TestCase subclasses)

Hierarchy of Django unit testing classes

You can convert a normal unittest.TestCase to any of the subclasses:
change the base class of your test from unittest.TestCase to the subclass.
All of the standard Python unit test functionality will be available, and it
will be augmented with some useful additions as described in each section
below.

SimpleTestCase

class SimpleTestCase

A subclass of unittest.TestCase that adds this functionality:

  • Some useful assertions like:
    • Checking that a callable raises a certain exception.
    • Checking that a callable triggers a certain warning.
    • Testing form field rendering and error treatment.
    • Testing HTML responses for the presence/lack of a given fragment.
    • Verifying that a template has/hasn't been used to generate a given
      response content
      .
    • Verifying that two URLs are equal.
    • Verifying an HTTP redirect is
      performed by the app.
    • Robustly testing two HTML fragments
      for equality/inequality or containment.
    • Robustly testing two XML fragments
      for equality/inequality.
    • Robustly testing two JSON fragments
      for equality.
  • The ability to run tests with modified settings.
  • Using the client Client.

If your tests make any database queries, use subclasses
TransactionTestCase or TestCase.

SimpleTestCase.databases

SimpleTestCase disallows database queries by default. This
helps to avoid executing write queries which will affect other tests
since each SimpleTestCase test isn’t run in a transaction. If you
aren’t concerned about this problem, you can disable this behavior by
setting the databases class attribute to '__all__' on your test
class.

Warning

SimpleTestCase and its subclasses (e.g. TestCase, …) rely on
setUpClass() and tearDownClass() to perform some class-wide
initialization (e.g. overriding settings). If you need to override those
methods, don’t forget to call the super implementation:

class MyTestCase(TestCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

    @classmethod
    def tearDownClass(cls):
        ...
        super().tearDownClass()

Be sure to account for Python’s behavior if an exception is raised during
setUpClass(). If that happens, neither the tests in the class nor
tearDownClass() are run. In the case of django.test.TestCase,
this will leak the transaction created in super() which results in
various symptoms including a segmentation fault on some platforms (reported
on macOS). If you want to intentionally raise an exception such as
unittest.SkipTest in setUpClass(), be sure to do it before
calling super() to avoid this.

TransactionTestCase

class TransactionTestCase

TransactionTestCase inherits from SimpleTestCase to
add some database-specific features:

  • Resetting the database to a known state at the beginning of each test to
    ease testing and using the ORM.
  • Database fixtures.
  • Test skipping based on database backend features.
  • The remaining specialized assert* methods.

Django’s TestCase class is a more commonly used subclass of
TransactionTestCase that makes use of database transaction facilities
to speed up the process of resetting the database to a known state at the
beginning of each test. A consequence of this, however, is that some database
behaviors cannot be tested within a Django TestCase class. For instance,
you cannot test that a block of code is executing within a transaction, as is
required when using
select_for_update(). In those cases,
you should use TransactionTestCase.

TransactionTestCase and TestCase are identical except for the manner
in which the database is reset to a known state and the ability for test code
to test the effects of commit and rollback:

  • A TransactionTestCase resets the database after the test runs by
    truncating all tables. A TransactionTestCase may call commit and rollback
    and observe the effects of these calls on the database.
  • A TestCase, on the other hand, does not truncate tables after a test.
    Instead, it encloses the test code in a database transaction that is rolled
    back at the end of the test. This guarantees that the rollback at the end of
    the test restores the database to its initial state.

Warning

TestCase running on a database that does not support rollback (e.g. MySQL
with the MyISAM storage engine), and all instances of TransactionTestCase,
will roll back at the end of the test by deleting all data from the test
database.

Apps will not see their data reloaded;
if you need this functionality (for example, third-party apps should enable
this) you can set serialized_rollback = True inside the
TestCase body.

TestCase

class TestCase

This is the most common class to use for writing tests in Django. It inherits
from TransactionTestCase (and by extension SimpleTestCase).
If your Django application doesn’t use a database, use SimpleTestCase.

The class:

  • Wraps the tests within two nested atomic()
    blocks: one for the whole class and one for each test. Therefore, if you want
    to test some specific database transaction behavior, use
    TransactionTestCase.
  • Checks deferrable database constraints at the end of each test.

It also provides an additional method:

classmethod TestCase.setUpTestData()¶

The class-level atomic block described above allows the creation of
initial data at the class level, once for the whole TestCase. This
technique allows for faster tests as compared to using setUp().

For example:

from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Note that if the tests are run on a database with no transaction support
(for instance, MySQL with the MyISAM engine), setUpTestData() will be
called before each test, negating the speed benefits.

Objects assigned to class attributes in setUpTestData() must support
creating deep copies with copy.deepcopy() in order to isolate them
from alterations performed by each test methods.

classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False

Returns a context manager that captures transaction.on_commit() callbacks for the given database
connection. It returns a list that contains, on exit of the context, the
captured callback functions. From this list you can make assertions on the
callbacks or call them to invoke their side effects, emulating a commit.

using is the alias of the database connection to capture callbacks for.

If execute is True, all the callbacks will be called as the context
manager exits, if no exception occurred. This emulates a commit after the
wrapped block of code.

For example:

from django.core import mail
from django.test import TestCase


class ContactTests(TestCase):
    def test_post(self):
        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            response = self.client.post(
                '/contact/',
                {'message': 'I like your site'},
            )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(callbacks), 1)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, 'Contact Form')
        self.assertEqual(mail.outbox[0].body, 'I like your site')

Changed in Django 4.0:

In older versions, new callbacks added while executing
transaction.on_commit() callbacks were not captured.

LiveServerTestCase

class LiveServerTestCase

LiveServerTestCase does basically the same as
TransactionTestCase with one extra feature: it launches a
live Django server in the background on setup, and shuts it down on teardown.
This allows the use of automated test clients other than the
Django dummy client such as, for example, the Selenium
client, to execute a series of functional tests inside a browser and simulate a
real user’s actions.

The live server listens on localhost and binds to port 0 which uses a free
port assigned by the operating system. The server’s URL can be accessed with
self.live_server_url during the tests.

To demonstrate how to use LiveServerTestCase, let’s write a Selenium test.
First of all, you need to install the selenium package into your Python
path:

/

$ python -m pip install selenium
...> py -m pip install selenium

Then, add a LiveServerTestCase-based test to your app’s tests module
(for example: myapp/tests.py). For this example, we’ll assume you’re using
the staticfiles app and want to have static files served
during the execution of your tests similar to what we get at development time
with DEBUG=True, i.e. without having to collect them using
collectstatic. We’ll use
the StaticLiveServerTestCase
subclass which provides that functionality. Replace it with
django.test.LiveServerTestCase if you don’t need that.

The code for this test may look as follows:

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver

class MySeleniumTests(StaticLiveServerTestCase):
    fixtures = ['user-data.json']

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
        username_input = self.selenium.find_element(By.NAME, "username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element(By.NAME, "password")
        password_input.send_keys('secret')
        self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()

Finally, you may run the test as follows:

/

$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...> manage.py test myapp.tests.MySeleniumTests.test_login

This example will automatically open Firefox then go to the login page, enter
the credentials and press the “Log in” button. Selenium offers other drivers in
case you do not have Firefox installed or wish to use another browser. The
example above is just a tiny fraction of what the Selenium client can do; check
out the full reference for more details.

Note

When using an in-memory SQLite database to run the tests, the same database
connection will be shared by two threads in parallel: the thread in which
the live server is run and the thread in which the test case is run. It’s
important to prevent simultaneous database queries via this shared
connection by the two threads, as that may sometimes randomly cause the
tests to fail. So you need to ensure that the two threads don’t access the
database at the same time. In particular, this means that in some cases
(for example, just after clicking a link or submitting a form), you might
need to check that a response is received by Selenium and that the next
page is loaded before proceeding with further test execution.
Do this, for example, by making Selenium wait until the <body> HTML tag
is found in the response (requires Selenium > 2.13):

def test_login(self):
    from selenium.webdriver.support.wait import WebDriverWait
    timeout = 2
    ...
    self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
    # Wait until the response is received
    WebDriverWait(self.selenium, timeout).until(
        lambda driver: driver.find_element(By.TAG_NAME, 'body'))

The tricky thing here is that there’s really no such thing as a “page load,”
especially in modern web apps that generate HTML dynamically after the
server generates the initial document. So, checking for the presence of
<body> in the response might not necessarily be appropriate for all use
cases. Please refer to the Selenium FAQ and Selenium documentation
for more information.

Test cases features¶

Default test client¶

SimpleTestCase.client

Every test case in a django.test.*TestCase instance has access to an
instance of a Django test client. This client can be accessed as
self.client. This client is recreated for each test, so you don’t have to
worry about state (such as cookies) carrying over from one test to another.

This means, instead of instantiating a Client in each test:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def test_details(self):
        client = Client()
        response = client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        client = Client()
        response = client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

…you can refer to self.client, like so:

from django.test import TestCase

class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

Customizing the test client¶

SimpleTestCase.client_class

If you want to use a different Client class (for example, a subclass
with customized behavior), use the client_class class
attribute:

from django.test import Client, TestCase

class MyTestClient(Client):
    # Specialized methods for your environment
    ...

class MyTest(TestCase):
    client_class = MyTestClient

    def test_my_stuff(self):
        # Here self.client is an instance of MyTestClient...
        call_some_test_code()

Fixture loading¶

TransactionTestCase.fixtures

A test case for a database-backed website isn’t much use if there isn’t any
data in the database. Tests are more readable and it’s more maintainable to
create objects using the ORM, for example in TestCase.setUpTestData(),
however, you can also use fixtures.

A fixture is a collection of data that Django knows how to import into a
database. For example, if your site has user accounts, you might set up a
fixture of fake user accounts in order to populate your database during tests.

The most straightforward way of creating a fixture is to use the
manage.py dumpdata command. This assumes you
already have some data in your database. See the dumpdata
documentation
for more details.

Once you’ve created a fixture and placed it in a fixtures directory in one
of your INSTALLED_APPS, you can use it in your unit tests by
specifying a fixtures class attribute on your django.test.TestCase
subclass:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    fixtures = ['mammals.json', 'birds']

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

Here’s specifically what will happen:

  • At the start of each test, before setUp() is run, Django will flush the
    database, returning the database to the state it was in directly after
    migrate was called.
  • Then, all the named fixtures are installed. In this example, Django will
    install any JSON fixture named mammals, followed by any fixture named
    birds. See the loaddata documentation for more
    details on defining and installing fixtures.

For performance reasons, TestCase loads fixtures once for the entire
test class, before setUpTestData(), instead of before each
test, and it uses transactions to clean the database before each test. In any case,
you can be certain that the outcome of a test will not be affected by another
test or by the order of test execution.

By default, fixtures are only loaded into the default database. If you are
using multiple databases and set TransactionTestCase.databases,
fixtures will be loaded into all specified databases.

URLconf configuration¶

If your application provides views, you may want to include tests that use the
test client to exercise those views. However, an end user is free to deploy the
views in your application at any URL of their choosing. This means that your
tests can’t rely upon the fact that your views will be available at a
particular URL. Decorate your test class or test method with
@override_settings(ROOT_URLCONF=...) for URLconf configuration.

Multi-database support¶

TransactionTestCase.databases

Django sets up a test database corresponding to every database that is
defined in the DATABASES definition in your settings and referred to
by at least one test through databases.

However, a big part of the time taken to run a Django TestCase is consumed
by the call to flush that ensures that you have a clean database at the
start of each test run. If you have multiple databases, multiple flushes are
required (one for each database), which can be a time consuming activity –
especially if your tests don’t need to test multi-database activity.

As an optimization, Django only flushes the default database at
the start of each test run. If your setup contains multiple databases,
and you have a test that requires every database to be clean, you can
use the databases attribute on the test suite to request extra databases
to be flushed.

For example:

class TestMyViews(TransactionTestCase):
    databases = {'default', 'other'}

    def test_index_page_view(self):
        call_some_test_code()

This test case will flush the default and other test databases before
running test_index_page_view. You can also use '__all__' to specify
that all of the test databases must be flushed.

The databases flag also controls which databases the
TransactionTestCase.fixtures are loaded into. By default, fixtures are
only loaded into the default database.

Queries against databases not in databases will give assertion errors to
prevent state leaking between tests.

TestCase.databases

By default, only the default database will be wrapped in a transaction
during a TestCase’s execution and attempts to query other databases will
result in assertion errors to prevent state leaking between tests.

Use the databases class attribute on the test class to request transaction
wrapping against non-default databases.

For example:

class OtherDBTests(TestCase):
    databases = {'other'}

    def test_other_db_query(self):
        ...

This test will only allow queries against the other database. Just like for
SimpleTestCase.databases and TransactionTestCase.databases, the
'__all__' constant can be used to specify that the test should allow
queries to all databases.

Overriding settings¶

Warning

Use the functions below to temporarily alter the value of settings in tests.
Don’t manipulate django.conf.settings directly as Django won’t restore
the original values after such manipulations.

SimpleTestCase.settings()¶

For testing purposes it’s often useful to change a setting temporarily and
revert to the original value after running the testing code. For this use case
Django provides a standard Python context manager (see PEP 343) called
settings(), which can be used like this:

from django.test import TestCase

class LoginTestCase(TestCase):

    def test_login(self):

        # First check for the default behavior
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/')

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL='/other/login/'):
            response = self.client.get('/sekrit/')
            self.assertRedirects(response, '/other/login/?next=/sekrit/')

This example will override the LOGIN_URL setting for the code
in the with block and reset its value to the previous state afterward.

SimpleTestCase.modify_settings()¶

It can prove unwieldy to redefine settings that contain a list of values. In
practice, adding or removing values is often sufficient. Django provides the
modify_settings() context manager for easier
settings changes:

from django.test import TestCase

class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        with self.modify_settings(MIDDLEWARE={
            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
            'remove': [
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
                'django.contrib.messages.middleware.MessageMiddleware',
            ],
        }):
            response = self.client.get('/')
            # ...

For each action, you can supply either a list of values or a string. When the
value already exists in the list, append and prepend have no effect;
neither does remove when the value doesn’t exist.

override_settings(**kwargs

In case you want to override a setting for a test method, Django provides the
override_settings() decorator (see PEP 318). It’s used
like this:

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

    @override_settings(LOGIN_URL='/other/login/')
    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

The decorator can also be applied to TestCase classes:

from django.test import TestCase, override_settings

@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):

    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')
modify_settings(*args, **kwargs

Likewise, Django provides the modify_settings()
decorator:

from django.test import TestCase, modify_settings

class MiddlewareTestCase(TestCase):

    @modify_settings(MIDDLEWARE={
        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
    })
    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

The decorator can also be applied to test case classes:

from django.test import TestCase, modify_settings

@modify_settings(MIDDLEWARE={
    'append': 'django.middleware.cache.FetchFromCacheMiddleware',
    'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

Note

When given a class, these decorators modify the class directly and return
it; they don’t create and return a modified copy of it. So if you try to
tweak the above examples to assign the return value to a different name
than LoginTestCase or MiddlewareTestCase, you may be surprised to
find that the original test case classes are still equally affected by the
decorator. For a given class, modify_settings() is
always applied after override_settings().

Warning

The settings file contains some settings that are only consulted during
initialization of Django internals. If you change them with
override_settings, the setting is changed if you access it via the
django.conf.settings module, however, Django’s internals access it
differently. Effectively, using override_settings() or
modify_settings() with these settings is probably not
going to do what you expect it to do.

We do not recommend altering the DATABASES setting. Altering
the CACHES setting is possible, but a bit tricky if you are
using internals that make using of caching, like
django.contrib.sessions. For example, you will have to reinitialize
the session backend in a test that uses cached sessions and overrides
CACHES.

Finally, avoid aliasing your settings as module-level constants as
override_settings() won’t work on such values since they are
only evaluated the first time the module is imported.

You can also simulate the absence of a setting by deleting it after settings
have been overridden, like this:

@override_settings()
def test_something(self):
    del settings.LOGIN_URL
    ...

When overriding settings, make sure to handle the cases in which your app’s
code uses a cache or similar feature that retains state even if the setting is
changed. Django provides the django.test.signals.setting_changed
signal that lets you register callbacks to clean up and otherwise reset state
when settings are changed.

Django itself uses this signal to reset various data:

Overridden settings Data reset
USE_TZ, TIME_ZONE Databases timezone
TEMPLATES Template engines
SERIALIZATION_MODULES Serializers cache
LOCALE_PATHS, LANGUAGE_CODE Default translation and loaded translations
MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage

Isolating apps¶

utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None

Registers the models defined within a wrapped context into their own
isolated apps registry. This functionality is useful
when creating model classes for tests, as the classes will be cleanly
deleted afterward, and there is no risk of name collisions.

The app labels which the isolated registry should contain must be passed as
individual arguments. You can use isolate_apps() as a decorator or a
context manager. For example:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

class MyModelTests(SimpleTestCase):

    @isolate_apps("app_label")
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        ...

… or:

with isolate_apps("app_label"):
    class TestModel(models.Model):
        pass
    ...

The decorator form can also be applied to classes.

Two optional keyword arguments can be specified:

  • attr_name: attribute assigned the isolated registry if used as a
    class decorator.
  • kwarg_name: keyword argument passing the isolated registry if used as
    a function decorator.

The temporary Apps instance used to isolate model registration can be
retrieved as an attribute when used as a class decorator by using the
attr_name parameter:

@isolate_apps("app_label", attr_name="apps")
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)

… or alternatively as an argument on the test method when used as a method
decorator by using the kwarg_name parameter:

class TestModelDefinition(SimpleTestCase):
    @isolate_apps("app_label", kwarg_name="apps")
    def test_model_definition(self, apps):
        class TestModel(models.Model):
            pass
        self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)

Emptying the test outbox¶

If you use any of Django’s custom TestCase classes, the test runner will
clear the contents of the test email outbox at the start of each test case.

For more detail on email services during tests, see Email services below.

Assertions¶

As Python’s normal unittest.TestCase class implements assertion methods
such as assertTrue() and
assertEqual(), Django’s custom TestCase class
provides a number of custom assertion methods that are useful for testing web
applications:

The failure messages given by most of these assertion methods can be customized
with the msg_prefix argument. This string will be prefixed to any failure
message generated by the assertion. This allows you to provide additional
details that may help you to identify the location and cause of a failure in
your test suite.

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

Asserts that execution of callable raises expected_exception and
that expected_message is found in the exception’s message. Any other
outcome is reported as a failure. It’s a simpler version of
unittest.TestCase.assertRaisesRegex() with the difference that
expected_message isn’t treated as a regular expression.

If only the expected_exception and expected_message parameters are
given, returns a context manager so that the code being tested can be
written inline rather than as a function:

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

Analogous to SimpleTestCase.assertRaisesMessage() but for
assertWarnsRegex() instead of
assertRaisesRegex().

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=»

Asserts that a form field behaves correctly with various inputs.

Parameters:
  • fieldclass – the class of the field to be tested.
  • valid – a dictionary mapping valid inputs to their expected cleaned
    values.
  • invalid – a dictionary mapping invalid inputs to one or more raised
    error messages.
  • field_args – the args passed to instantiate the field.
  • field_kwargs – the kwargs passed to instantiate the field.
  • empty_value – the expected clean output for inputs in empty_values.

For example, the following code tests that an EmailField accepts
a@a.com as a valid email address, but rejects aaa with a reasonable
error message:

self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase.assertFormError(form, field, errors, msg_prefix=»

Asserts that a field on a form raises the provided list of errors.

form is a Form instance. The form must be
bound but not necessarily
validated (assertFormError() will automatically call full_clean()
on the form).

field is the name of the field on the form to check. To check the form’s
non-field errors, use
field=None.

errors is a list of all the error strings that the field is expected to
have. You can also pass a single error string if you only expect one error
which means that errors='error message' is the same as
errors=['error message'].

Changed in Django 4.1:

In older versions, using an empty error list with assertFormError()
would always pass, regardless of whether the field had any errors or
not. Starting from Django 4.1, using errors=[] will only pass if
the field actually has no errors.

Django 4.1 also changed the behavior of assertFormError() when a
field has multiple errors. In older versions, if a field had multiple
errors and you checked for only some of them, the test would pass.
Starting from Django 4.1, the error list must be an exact match to the
field’s actual errors.

Deprecated since version 4.1: Support for passing a response object and a form name to
assertFormError() is deprecated and will be removed in Django 5.0.
Use the form instance directly instead.

SimpleTestCase.assertFormsetError(formset, form_index, field, errors, msg_prefix=»

Asserts that the formset raises the provided list of errors when
rendered.

formset is a Formset instance. The formset must be bound but not
necessarily validated (assertFormsetError() will automatically call the
full_clean() on the formset).

form_index is the number of the form within the Formset (starting
from 0). Use form_index=None to check the formset’s non-form errors,
i.e. the errors you get when calling formset.non_form_errors(). In that
case you must also use field=None.

field and errors have the same meaning as the parameters to
assertFormError().

Deprecated since version 4.1: Support for passing a response object and a formset name to
assertFormsetError() is deprecated and will be removed in Django
5.0. Use the formset instance directly instead.

SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix=», html=False

Asserts that a response produced the
given status_code and that text
appears in its content. If count
is provided, text must occur exactly count times in the response.

Set html to True to handle text as HTML. The comparison with
the response content will be based on HTML semantics instead of
character-by-character equality. Whitespace is ignored in most cases,
attribute ordering is not significant. See
assertHTMLEqual() for more details.

SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix=», html=False

Asserts that a response produced the
given status_code and that text does
not appear in its content.

Set html to True to handle text as HTML. The comparison with
the response content will be based on HTML semantics instead of
character-by-character equality. Whitespace is ignored in most cases,
attribute ordering is not significant. See
assertHTMLEqual() for more details.

SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix=», count=None

Asserts that the template with the given name was used in rendering the
response.

response must be a response instance returned by the
test client.

template_name should be a string such as 'admin/index.html'.

The count argument is an integer indicating the number of times the
template should be rendered. Default is None, meaning that the template
should be rendered one or more times.

You can use this as a context manager, like this:

with self.assertTemplateUsed('index.html'):
    render_to_string('index.html')
with self.assertTemplateUsed(template_name='index.html'):
    render_to_string('index.html')
SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix=»

Asserts that the template with the given name was not used in rendering
the response.

You can use this as a context manager in the same way as
assertTemplateUsed().

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix=»

Asserts that two URLs are the same, ignoring the order of query string
parameters except for parameters with the same name. For example,
/path/?x=1&y=2 is equal to /path/?y=2&x=1, but
/path/?a=1&a=2 isn’t equal to /path/?a=2&a=1.

SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix=», fetch_redirect_response=True

Asserts that the response returned a
status_code redirect status, redirected
to expected_url (including any GET data), and that the final page
was received with target_status_code.

If your request used the follow argument, the expected_url and
target_status_code will be the url and status code for the final
point of the redirect chain.

If fetch_redirect_response is False, the final page won’t be
loaded. Since the test client can’t fetch external URLs, this is
particularly useful if expected_url isn’t part of your Django app.

Scheme is handled correctly when making comparisons between two URLs. If
there isn’t any scheme specified in the location where we are redirected to,
the original request’s scheme is used. If present, the scheme in
expected_url is the one used to make the comparisons to.

SimpleTestCase.assertHTMLEqual(html1, html2, msg=None

Asserts that the strings html1 and html2 are equal. The comparison
is based on HTML semantics. The comparison takes following things into
account:

  • Whitespace before and after HTML tags is ignored.
  • All types of whitespace are considered equivalent.
  • All open tags are closed implicitly, e.g. when a surrounding tag is
    closed or the HTML document ends.
  • Empty tags are equivalent to their self-closing version.
  • The ordering of attributes of an HTML element is not significant.
  • Boolean attributes (like checked) without an argument are equal to
    attributes that equal in name and value (see the examples).
  • Text, character references, and entity references that refer to the same
    character are equivalent.

The following examples are valid tests and don’t raise any
AssertionError:

self.assertHTMLEqual(
    '<p>Hello <b>'world'!</p>',
    '''<p>
        Hello   <b>'world'! </b>
    </p>'''
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>'
)

html1 and html2 must contain HTML. An AssertionError will be
raised if one of them cannot be parsed.

Output in case of error can be customized with the msg argument.

Changed in Django 4.0:

In older versions, any attribute (not only boolean attributes) without
a value was considered equal to an attribute with the same name and
value.

SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None

Asserts that the strings html1 and html2 are not equal. The
comparison is based on HTML semantics. See
assertHTMLEqual() for details.

html1 and html2 must contain HTML. An AssertionError will be
raised if one of them cannot be parsed.

Output in case of error can be customized with the msg argument.

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None

Asserts that the strings xml1 and xml2 are equal. The
comparison is based on XML semantics. Similarly to
assertHTMLEqual(), the comparison is
made on parsed content, hence only semantic differences are considered, not
syntax differences. When invalid XML is passed in any parameter, an
AssertionError is always raised, even if both strings are identical.

XML declaration, document type, processing instructions, and comments are
ignored. Only the root element and its children are compared.

Output in case of error can be customized with the msg argument.

SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None

Asserts that the strings xml1 and xml2 are not equal. The
comparison is based on XML semantics. See
assertXMLEqual() for details.

Output in case of error can be customized with the msg argument.

SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix=»

Asserts that the HTML fragment needle is contained in the haystack
once.

If the count integer argument is specified, then additionally the number
of needle occurrences will be strictly verified.

Whitespace in most cases is ignored, and attribute ordering is not
significant. See assertHTMLEqual() for more details.

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None

Asserts that the JSON fragments raw and expected_data are equal.
Usual JSON non-significant whitespace rules apply as the heavyweight is
delegated to the json library.

Output in case of error can be customized with the msg argument.

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None

Asserts that the JSON fragments raw and expected_data are not equal.
See assertJSONEqual() for further details.

Output in case of error can be customized with the msg argument.

TransactionTestCase.assertQuerysetEqual(qs, values, transform=None, ordered=True, msg=None

Asserts that a queryset qs matches a particular iterable of values
values.

If transform is provided, values is compared to a list produced by
applying transform to each member of qs.

By default, the comparison is also ordering dependent. If qs doesn’t
provide an implicit ordering, you can set the ordered parameter to
False, which turns the comparison into a collections.Counter comparison.
If the order is undefined (if the given qs isn’t ordered and the
comparison is against more than one ordered value), a ValueError is
raised.

Output in case of error can be customized with the msg argument.

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs

Asserts that when func is called with *args and **kwargs that
num database queries are executed.

If a "using" key is present in kwargs it is used as the database
alias for which to check the number of queries:

self.assertNumQueries(7, using='non_default_db')

If you wish to call a function with a using parameter you can do it by
wrapping the call with a lambda to add an extra parameter:

self.assertNumQueries(7, lambda: my_function(using=7))

You can also use this as a context manager:

with self.assertNumQueries(2):
    Person.objects.create(name="Aaron")
    Person.objects.create(name="Daniel")

Tagging tests¶

You can tag your tests so you can easily run a particular subset. For example,
you might label fast or slow tests:

from django.test import tag

class SampleTestCase(TestCase):

    @tag('fast')
    def test_fast(self):
        ...

    @tag('slow')
    def test_slow(self):
        ...

    @tag('slow', 'core')
    def test_slow_but_core(self):
        ...

You can also tag a test case:

@tag('slow', 'core')
class SampleTestCase(TestCase):
    ...

Subclasses inherit tags from superclasses, and methods inherit tags from their
class. Given:

@tag('foo')
class SampleTestCaseChild(SampleTestCase):

    @tag('bar')
    def test(self):
        ...

SampleTestCaseChild.test will be labeled with 'slow', 'core',
'bar', and 'foo'.

Then you can choose which tests to run. For example, to run only fast tests:

/

$ ./manage.py test --tag=fast
...> manage.py test --tag=fast

Or to run fast tests and the core one (even though it’s slow):

/

$ ./manage.py test --tag=fast --tag=core
...> manage.py test --tag=fast --tag=core

You can also exclude tests by tag. To run core tests if they are not slow:

/

$ ./manage.py test --tag=core --exclude-tag=slow
...> manage.py test --tag=core --exclude-tag=slow

test --exclude-tag has precedence over test --tag, so if a
test has two tags and you select one of them and exclude the other, the test
won’t be run.

Testing asynchronous code¶

If you merely want to test the output of your asynchronous views, the standard
test client will run them inside their own asynchronous loop without any extra
work needed on your part.

However, if you want to write fully-asynchronous tests for a Django project,
you will need to take several things into account.

Firstly, your tests must be async def methods on the test class (in order
to give them an asynchronous context). Django will automatically detect
any async def tests and wrap them so they run in their own event loop.

If you are testing from an asynchronous function, you must also use the
asynchronous test client. This is available as django.test.AsyncClient,
or as self.async_client on any test.

class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, **defaults

AsyncClient has the same methods and signatures as the synchronous (normal)
test client, with two exceptions:

  • In the initialization, arbitrary keyword arguments in defaults are added
    directly into the ASGI scope.

  • The follow parameter is not supported.

  • Headers passed as extra keyword arguments should not have the HTTP_
    prefix required by the synchronous client (see Client.get()). For
    example, here is how to set an HTTP Accept header:

    >>> c = AsyncClient()
    >>> c.get(
    ...     '/customers/details/',
    ...     {'name': 'fred', 'age': 7},
    ...     ACCEPT='application/json'
    ... )
    

Using AsyncClient any method that makes a request must be awaited:

async def test_my_thing(self):
    response = await self.async_client.get('/some-url/')
    self.assertEqual(response.status_code, 200)

The asynchronous client can also call synchronous views; it runs through
Django’s asynchronous request path, which supports both.
Any view called through the AsyncClient will get an ASGIRequest object
for its request rather than the WSGIRequest that the normal client
creates.

Warning

If you are using test decorators, they must be async-compatible to ensure
they work correctly. Django’s built-in decorators will behave correctly, but
third-party ones may appear to not execute (they will “wrap” the wrong part
of the execution flow and not your test).

If you need to use these decorators, then you should decorate your test
methods with async_to_sync() inside of them instead:

from asgiref.sync import async_to_sync
from django.test import TestCase

class MyTests(TestCase):

    @mock.patch(...)
    @async_to_sync
    async def test_my_thing(self):
        ...

Email services¶

If any of your Django views send email using Django’s email
functionality
, you probably don’t want to send email each time
you run a test using that view. For this reason, Django’s test runner
automatically redirects all Django-sent email to a dummy outbox. This lets you
test every aspect of sending email – from the number of messages sent to the
contents of each message – without actually sending the messages.

The test runner accomplishes this by transparently replacing the normal
email backend with a testing backend.
(Don’t worry – this has no effect on any other email senders outside of
Django, such as your machine’s mail server, if you’re running one.)

django.core.mail.outbox

During test running, each outgoing email is saved in
django.core.mail.outbox. This is a list of all
EmailMessage instances that have been sent. The
outbox attribute is a special attribute that is created only when the
locmem email backend is used. It doesn’t normally exist as part of the
django.core.mail module and you can’t import it directly. The code below
shows how to access this attribute correctly.

Here’s an example test that examines django.core.mail.outbox for length
and contents:

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            'Subject here', 'Here is the message.',
            'from@example.com', ['to@example.com'],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

As noted previously, the test outbox is emptied
at the start of every test in a Django *TestCase. To empty the outbox
manually, assign the empty list to mail.outbox:

from django.core import mail

# Empty the test outbox
mail.outbox = []

Management Commands¶

Management commands can be tested with the
call_command() function. The output can be
redirected into a StringIO instance:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

Skipping tests¶

The unittest library provides the @skipIf and
@skipUnless decorators to allow you to skip tests
if you know ahead of time that those tests are going to fail under certain
conditions.

For example, if your test requires a particular optional library in order to
succeed, you could decorate the test case with @skipIf. Then, the test runner will report that the test wasn’t
executed and why, instead of failing the test or omitting the test altogether.

To supplement these test skipping behaviors, Django provides two
additional skip decorators. Instead of testing a generic boolean,
these decorators check the capabilities of the database, and skip the
test if the database doesn’t support a specific named feature.

The decorators use a string identifier to describe database features.
This string corresponds to attributes of the database connection
features class. See
django.db.backends.base.features.BaseDatabaseFeatures class for a full list of database features
that can be used as a basis for skipping tests.

skipIfDBFeature(*feature_name_strings

Skip the decorated test or TestCase if all of the named database features
are supported.

For example, the following test will not be executed if the database
supports transactions (e.g., it would not run under PostgreSQL, but
it would under MySQL with MyISAM tables):

class MyTests(TestCase):
    @skipIfDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings

Skip the decorated test or TestCase if any of the named database features
are not supported.

For example, the following test will only be executed if the database
supports transactions (e.g., it would run under PostgreSQL, but not
under MySQL with MyISAM tables):

class MyTests(TestCase):
    @skipUnlessDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass

this is on Django 1.6

def test_perfil_password_validates_new_passwords(self):
    self.client.login(username='test@test.com', password='test')
    resp = self.client.post('/perfil/password/',
                            json.dumps({'oldpassword': 'test',
                                        'newPassword1': 'wrong',
                                        'newPassword2': 'nuevo'}
                                       ),
                            'text/json',
                            HTTP_X_REQUESTED_WITH='XMLHttpRequest')
    self.assertEqual(resp.status_code, 400)

THIS WORKS and this is the output:

Creating test database for alias 'default'... .
---------------------------------------------------------------------- 
Ran 1 test in 0.273s

OK

Now if i add this extra assert

self.assertContains(resp, '"error":')

Creating test database for alias 'default'... F
====================================================================== 
FAIL: test_perfil_password_validates_new_passwords (users.tests.PerfilLoggedTestCase)
----------------------------------------------------------------------  
Traceback (most recent call last):   File "/.../src/users/tests.py", 
line 141, in test_perfil_password_validates_new_passwords
    self.assertContains(resp, '"error":')   
File "/usr/local/lib/python2.7/dist-packages/django/test/testcases.py", line 327, 
in assertContains
    " (expected %d)" % (response.status_code, status_code)) 
AssertionError: Couldn't retrieve content: Response code was 400 (expected 200)

---------------------------------------------------------------------- 
Ran 1 test in 0.241s

FAILED (failures=1)

I have no clue why this expects 200, or why does it give me an assertionError I can even print the content, so it’s there. What am I missing?

import difflib import json import logging import posixpath import sys import threading import unittest import warnings from collections import Counter from contextlib import contextmanager from copy import copy, deepcopy from difflib import get_close_matches from functools import wraps from unittest.suite import _DebugResult from unittest.util import safe_repr from urllib.parse import ( parse_qsl, unquote, urlencode, urljoin, urlparse, urlsplit, urlunparse, ) from urllib.request import url2pathname from asgiref.sync import async_to_sync, iscoroutinefunction from django.apps import apps from django.conf import settings from django.core import mail from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.files import locks from django.core.handlers.wsgi import WSGIHandler, get_path_info from django.core.management import call_command from django.core.management.color import no_style from django.core.management.sql import emit_post_migrate_signal from django.core.servers.basehttp import ThreadedWSGIServer, WSGIRequestHandler from django.core.signals import setting_changed from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction from django.forms.fields import CharField from django.http import QueryDict from django.http.request import split_domain_port, validate_host from django.test.client import AsyncClient, Client from django.test.html import HTMLParseError, parse_html from django.test.signals import template_rendered from django.test.utils import ( CaptureQueriesContext, ContextList, compare_xml, modify_settings, override_settings, ) from django.utils.deprecation import RemovedInDjango51Warning from django.utils.functional import classproperty from django.views.static import serve logger = logging.getLogger(«django.test») __all__ = ( «TestCase», «TransactionTestCase», «SimpleTestCase», «skipIfDBFeature», «skipUnlessDBFeature», ) def to_list(value): «»»Put value into a list if it’s not already one.»»» if not isinstance(value, list): value = [value] return value def assert_and_parse_html(self, html, user_msg, msg): try: dom = parse_html(html) except HTMLParseError as e: standardMsg = «%sn%s» % (msg, e) self.fail(self._formatMessage(user_msg, standardMsg)) return dom class _AssertNumQueriesContext(CaptureQueriesContext): def __init__(self, test_case, num, connection): self.test_case = test_case self.num = num super().__init__(connection) def __exit__(self, exc_type, exc_value, traceback): super().__exit__(exc_type, exc_value, traceback) if exc_type is not None: return executed = len(self) self.test_case.assertEqual( executed, self.num, «%d queries executed, %d expectednCaptured queries were:n%s» % ( executed, self.num, «n«.join( «%d. %s» % (i, query[«sql»]) for i, query in enumerate(self.captured_queries, start=1) ), ), ) class _AssertTemplateUsedContext: def __init__(self, test_case, template_name, msg_prefix=«», count=None): self.test_case = test_case self.template_name = template_name self.msg_prefix = msg_prefix self.count = count self.rendered_templates = [] self.rendered_template_names = [] self.context = ContextList() def on_template_render(self, sender, signal, template, context, **kwargs): self.rendered_templates.append(template) self.rendered_template_names.append(template.name) self.context.append(copy(context)) def test(self): self.test_case._assert_template_used( self.template_name, self.rendered_template_names, self.msg_prefix, self.count, ) def __enter__(self): template_rendered.connect(self.on_template_render) return self def __exit__(self, exc_type, exc_value, traceback): template_rendered.disconnect(self.on_template_render) if exc_type is not None: return self.test() class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): def test(self): self.test_case.assertFalse( self.template_name in self.rendered_template_names, {self.msg_prefix}Template ‘{self.template_name}‘ was used « f»unexpectedly in rendering the response», ) class DatabaseOperationForbidden(AssertionError): pass class _DatabaseFailure: def __init__(self, wrapped, message): self.wrapped = wrapped self.message = message def __call__(self): raise DatabaseOperationForbidden(self.message) class SimpleTestCase(unittest.TestCase): # The class we’ll use for the test client self.client. # Can be overridden in derived classes. client_class = Client async_client_class = AsyncClient _overridden_settings = None _modified_settings = None databases = set() _disallowed_database_msg = ( «Database %(operation)s to %(alias)r are not allowed in SimpleTestCase « «subclasses. Either subclass TestCase or TransactionTestCase to ensure « «proper test isolation or add %(alias)r to %(test)s.databases to silence « «this failure.» ) _disallowed_connection_methods = [ («connect», «connections»), («temporary_connection», «connections»), («cursor», «queries»), («chunked_cursor», «queries»), ] @classmethod def setUpClass(cls): super().setUpClass() if cls._overridden_settings: cls._cls_overridden_context = override_settings(**cls._overridden_settings) cls._cls_overridden_context.enable() cls.addClassCleanup(cls._cls_overridden_context.disable) if cls._modified_settings: cls._cls_modified_context = modify_settings(cls._modified_settings) cls._cls_modified_context.enable() cls.addClassCleanup(cls._cls_modified_context.disable) cls._add_databases_failures() cls.addClassCleanup(cls._remove_databases_failures) @classmethod def _validate_databases(cls): if cls.databases == «__all__»: return frozenset(connections) for alias in cls.databases: if alias not in connections: message = ( «%s.%s.databases refers to %r which is not defined in « «settings.DATABASES.» % ( cls.__module__, cls.__qualname__, alias, ) ) close_matches = get_close_matches(alias, list(connections)) if close_matches: message += » Did you mean %r?» % close_matches[0] raise ImproperlyConfigured(message) return frozenset(cls.databases) @classmethod def _add_databases_failures(cls): cls.databases = cls._validate_databases() for alias in connections: if alias in cls.databases: continue connection = connections[alias] for name, operation in cls._disallowed_connection_methods: message = cls._disallowed_database_msg % { «test»: «%s.%s» % (cls.__module__, cls.__qualname__), «alias»: alias, «operation»: operation, } method = getattr(connection, name) setattr(connection, name, _DatabaseFailure(method, message)) @classmethod def _remove_databases_failures(cls): for alias in connections: if alias in cls.databases: continue connection = connections[alias] for name, _ in cls._disallowed_connection_methods: method = getattr(connection, name) setattr(connection, name, method.wrapped) def __call__(self, result=None): «»» Wrapper around default __call__ method to perform common Django test set up. This means that user-defined Test Cases aren’t required to include a call to super().setUp(). «»» self._setup_and_call(result) def debug(self): «»»Perform the same as __call__(), without catching the exception.»»» debug_result = _DebugResult() self._setup_and_call(debug_result, debug=True) def _setup_and_call(self, result, debug=False): «»» Perform the following in order: pre-setup, run test, post-teardown, skipping pre/post hooks if test is set to be skipped. If debug=True, reraise any errors in setup and use super().debug() instead of __call__() to run the test. «»» testMethod = getattr(self, self._testMethodName) skipped = getattr(self.__class__, «__unittest_skip__», False) or getattr( testMethod, «__unittest_skip__», False ) # Convert async test methods. if iscoroutinefunction(testMethod): setattr(self, self._testMethodName, async_to_sync(testMethod)) if not skipped: try: self._pre_setup() except Exception: if debug: raise result.addError(self, sys.exc_info()) return if debug: super().debug() else: super().__call__(result) if not skipped: try: self._post_teardown() except Exception: if debug: raise result.addError(self, sys.exc_info()) return def _pre_setup(self): «»» Perform pre-test setup: * Create a test client. * Clear the mail test outbox. «»» self.client = self.client_class() self.async_client = self.async_client_class() mail.outbox = [] def _post_teardown(self): «»»Perform post-test things.»»» pass def settings(self, **kwargs): «»» A context manager that temporarily sets a setting and reverts to the original value when exiting the context. «»» return override_settings(**kwargs) def modify_settings(self, **kwargs): «»» A context manager that temporarily applies changes a list setting and reverts back to the original value when exiting the context. «»» return modify_settings(**kwargs) def assertRedirects( self, response, expected_url, status_code=302, target_status_code=200, msg_prefix=«», fetch_redirect_response=True, ): «»» Assert that a response redirected to a specific URL and that the redirect URL can be loaded. Won’t work for external links since it uses the test client to do a request (use fetch_redirect_response=False to check such links without fetching them). «»» if msg_prefix: msg_prefix += «: « if hasattr(response, «redirect_chain»): # The request was a followed redirect self.assertTrue( response.redirect_chain, msg_prefix + ( «Response didn’t redirect as expected: Response code was %d « «(expected %d)» ) % (response.status_code, status_code), ) self.assertEqual( response.redirect_chain[0][1], status_code, msg_prefix + ( «Initial response didn’t redirect as expected: Response code was « «%d (expected %d)» ) % (response.redirect_chain[0][1], status_code), ) url, status_code = response.redirect_chain[1] self.assertEqual( response.status_code, target_status_code, msg_prefix + ( «Response didn’t redirect as expected: Final Response code was %d « «(expected %d)» ) % (response.status_code, target_status_code), ) else: # Not a followed redirect self.assertEqual( response.status_code, status_code, msg_prefix + ( «Response didn’t redirect as expected: Response code was %d « «(expected %d)» ) % (response.status_code, status_code), ) url = response.url scheme, netloc, path, query, fragment = urlsplit(url) # Prepend the request path to handle relative path redirects. if not path.startswith(«/»): url = urljoin(response.request[«PATH_INFO»], url) path = urljoin(response.request[«PATH_INFO»], path) if fetch_redirect_response: # netloc might be empty, or in cases where Django tests the # HTTP scheme, the convention is for netloc to be ‘testserver’. # Trust both as «internal» URLs here. domain, port = split_domain_port(netloc) if domain and not validate_host(domain, settings.ALLOWED_HOSTS): raise ValueError( «The test client is unable to fetch remote URLs (got %s). « «If the host is served by Django, add ‘%s’ to ALLOWED_HOSTS. « «Otherwise, use « «assertRedirects(…, fetch_redirect_response=False).» % (url, domain) ) # Get the redirection page, using the same client that was used # to obtain the original response. extra = response.client.extra or {} headers = response.client.headers or {} redirect_response = response.client.get( path, QueryDict(query), secure=(scheme == «https»), headers=headers, **extra, ) self.assertEqual( redirect_response.status_code, target_status_code, msg_prefix + ( «Couldn’t retrieve redirection page ‘%s’: response code was %d « «(expected %d)» ) % (path, redirect_response.status_code, target_status_code), ) self.assertURLEqual( url, expected_url, msg_prefix + «Response redirected to ‘%s’, expected ‘%s'» % (url, expected_url), ) def assertURLEqual(self, url1, url2, msg_prefix=«»): «»» Assert that two URLs are the same, ignoring the order of query string parameters except for parameters with the same name. For example, /path/?x=1&y=2 is equal to /path/?y=2&x=1, but /path/?a=1&a=2 isn’t equal to /path/?a=2&a=1. «»» def normalize(url): «»»Sort the URL’s query string parameters.»»» url = str(url) # Coerce reverse_lazy() URLs. scheme, netloc, path, params, query, fragment = urlparse(url) query_parts = sorted(parse_qsl(query)) return urlunparse( (scheme, netloc, path, params, urlencode(query_parts), fragment) ) self.assertEqual( normalize(url1), normalize(url2), msg_prefix + «Expected ‘%s’ to equal ‘%s’.» % (url1, url2), ) def _assert_contains(self, response, text, status_code, msg_prefix, html): # If the response supports deferred rendering and hasn’t been rendered # yet, then ensure that it does get rendered before proceeding further. if ( hasattr(response, «render») and callable(response.render) and not response.is_rendered ): response.render() if msg_prefix: msg_prefix += «: « self.assertEqual( response.status_code, status_code, msg_prefix + «Couldn’t retrieve content: Response code was %d» » (expected %d)» % (response.status_code, status_code), ) if response.streaming: content = b»».join(response.streaming_content) else: content = response.content if not isinstance(text, bytes) or html: text = str(text) content = content.decode(response.charset) text_repr = «‘%s'» % text else: text_repr = repr(text) if html: content = assert_and_parse_html( self, content, None, «Response’s content is not valid HTML:» ) text = assert_and_parse_html( self, text, None, «Second argument is not valid HTML:» ) real_count = content.count(text) return (text_repr, real_count, msg_prefix) def assertContains( self, response, text, count=None, status_code=200, msg_prefix=«», html=False ): «»» Assert that a response indicates that some content was retrieved successfully, (i.e., the HTTP status code was as expected) and that «text« occurs «count« times in the content of the response. If «count« is None, the count doesn’t matter — the assertion is true if the text occurs at least once in the response. «»» text_repr, real_count, msg_prefix = self._assert_contains( response, text, status_code, msg_prefix, html ) if count is not None: self.assertEqual( real_count, count, msg_prefix + «Found %d instances of %s in response (expected %d)» % (real_count, text_repr, count), ) else: self.assertTrue( real_count != 0, msg_prefix + «Couldn’t find %s in response» % text_repr ) def assertNotContains( self, response, text, status_code=200, msg_prefix=«», html=False ): «»» Assert that a response indicates that some content was retrieved successfully, (i.e., the HTTP status code was as expected) and that «text« doesn’t occur in the content of the response. «»» text_repr, real_count, msg_prefix = self._assert_contains( response, text, status_code, msg_prefix, html ) self.assertEqual( real_count, 0, msg_prefix + «Response should not contain %s» % text_repr ) def _check_test_client_response(self, response, attribute, method_name): «»» Raise a ValueError if the given response doesn’t have the required attribute. «»» if not hasattr(response, attribute): raise ValueError( {method_name}() is only usable on responses fetched using « «the Django test Client.» ) def _assert_form_error(self, form, field, errors, msg_prefix, form_repr): if not form.is_bound: self.fail( {msg_prefix}The {form_repr} is not bound, it will never have any « f»errors.» ) if field is not None and field not in form.fields: self.fail( {msg_prefix}The {form_repr} does not contain the field {field!r} ) if field is None: field_errors = form.non_field_errors() failure_message = f»The non-field errors of {form_repr} don’t match.» else: field_errors = form.errors.get(field, []) failure_message = ( f»The errors of field {field!r} on {form_repr} don’t match.» ) self.assertEqual(field_errors, errors, msg_prefix + failure_message) def assertFormError(self, form, field, errors, msg_prefix=«»): «»» Assert that a field named «field» on the given form object has specific errors. errors can be either a single error message or a list of errors messages. Using errors=[] test that the field has no errors. You can pass field=None to check the form’s non-field errors. «»» if msg_prefix: msg_prefix += «: « errors = to_list(errors) self._assert_form_error(form, field, errors, msg_prefix, f»form {form!r}«) # RemovedInDjango51Warning. def assertFormsetError(self, *args, **kw): warnings.warn( «assertFormsetError() is deprecated in favor of assertFormSetError().», category=RemovedInDjango51Warning, stacklevel=2, ) return self.assertFormSetError(*args, **kw) def assertFormSetError(self, formset, form_index, field, errors, msg_prefix=«»): «»» Similar to assertFormError() but for formsets. Use form_index=None to check the formset’s non-form errors (in that case, you must also use field=None). Otherwise use an integer to check the formset’s n-th form for errors. Other parameters are the same as assertFormError(). «»» if form_index is None and field is not None: raise ValueError(«You must use field=None with form_index=None.») if msg_prefix: msg_prefix += «: « errors = to_list(errors) if not formset.is_bound: self.fail( {msg_prefix}The formset {formset!r} is not bound, it will never have « f»any errors.» ) if form_index is not None and form_index >= formset.total_form_count(): form_count = formset.total_form_count() form_or_forms = «forms» if form_count > 1 else «form» self.fail( {msg_prefix}The formset {formset!r} only has {form_count} « {form_or_forms} ) if form_index is not None: form_repr = f»form {form_index} of formset {formset!r}« self._assert_form_error( formset.forms[form_index], field, errors, msg_prefix, form_repr ) else: failure_message = f»The non-form errors of formset {formset!r} don’t match.» self.assertEqual( formset.non_form_errors(), errors, msg_prefix + failure_message ) def _get_template_used(self, response, template_name, msg_prefix, method_name): if response is None and template_name is None: raise TypeError(«response and/or template_name argument must be provided») if msg_prefix: msg_prefix += «: « if template_name is not None and response is not None: self._check_test_client_response(response, «templates», method_name) if not hasattr(response, «templates») or (response is None and template_name): if response: template_name = response response = None # use this template with context manager return template_name, None, msg_prefix template_names = [t.name for t in response.templates if t.name is not None] return None, template_names, msg_prefix def _assert_template_used(self, template_name, template_names, msg_prefix, count): if not template_names: self.fail(msg_prefix + «No templates used to render the response») self.assertTrue( template_name in template_names, msg_prefix + «Template ‘%s’ was not a template used to render» » the response. Actual template(s) used: %s» % (template_name, «, «.join(template_names)), ) if count is not None: self.assertEqual( template_names.count(template_name), count, msg_prefix + «Template ‘%s’ was expected to be rendered %d « «time(s) but was actually rendered %d time(s).» % (template_name, count, template_names.count(template_name)), ) def assertTemplateUsed( self, response=None, template_name=None, msg_prefix=«», count=None ): «»» Assert that the template with the provided name was used in rendering the response. Also usable as context manager. «»» context_mgr_template, template_names, msg_prefix = self._get_template_used( response, template_name, msg_prefix, «assertTemplateUsed», ) if context_mgr_template: # Use assertTemplateUsed as context manager. return _AssertTemplateUsedContext( self, context_mgr_template, msg_prefix, count ) self._assert_template_used(template_name, template_names, msg_prefix, count) def assertTemplateNotUsed(self, response=None, template_name=None, msg_prefix=«»): «»» Assert that the template with the provided name was NOT used in rendering the response. Also usable as context manager. «»» context_mgr_template, template_names, msg_prefix = self._get_template_used( response, template_name, msg_prefix, «assertTemplateNotUsed», ) if context_mgr_template: # Use assertTemplateNotUsed as context manager. return _AssertTemplateNotUsedContext(self, context_mgr_template, msg_prefix) self.assertFalse( template_name in template_names, msg_prefix + «Template ‘%s’ was used unexpectedly in rendering the response» % template_name, ) @contextmanager def _assert_raises_or_warns_cm( self, func, cm_attr, expected_exception, expected_message ): with func(expected_exception) as cm: yield cm self.assertIn(expected_message, str(getattr(cm, cm_attr))) def _assertFooMessage( self, func, cm_attr, expected_exception, expected_message, *args, **kwargs ): callable_obj = None if args: callable_obj, *args = args cm = self._assert_raises_or_warns_cm( func, cm_attr, expected_exception, expected_message ) # Assertion used in context manager fashion. if callable_obj is None: return cm # Assertion was passed a callable. with cm: callable_obj(*args, **kwargs) def assertRaisesMessage( self, expected_exception, expected_message, *args, **kwargs ): «»» Assert that expected_message is found in the message of a raised exception. Args: expected_exception: Exception class expected to be raised. expected_message: expected error message string value. args: Function to be called and extra positional args. kwargs: Extra kwargs. «»» return self._assertFooMessage( self.assertRaises, «exception», expected_exception, expected_message, *args, **kwargs, ) def assertWarnsMessage(self, expected_warning, expected_message, *args, **kwargs): «»» Same as assertRaisesMessage but for assertWarns() instead of assertRaises(). «»» return self._assertFooMessage( self.assertWarns, «warning», expected_warning, expected_message, *args, **kwargs, ) def assertFieldOutput( self, fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=«», ): «»» Assert that a form field behaves correctly with various inputs. Args: fieldclass: the class of the field to be tested. valid: a dictionary mapping valid inputs to their expected cleaned values. invalid: a dictionary mapping invalid inputs to one or more raised error messages. field_args: the args passed to instantiate the field field_kwargs: the kwargs passed to instantiate the field empty_value: the expected clean output for inputs in empty_values «»» if field_args is None: field_args = [] if field_kwargs is None: field_kwargs = {} required = fieldclass(*field_args, **field_kwargs) optional = fieldclass(*field_args, **{**field_kwargs, «required»: False}) # test valid inputs for input, output in valid.items(): self.assertEqual(required.clean(input), output) self.assertEqual(optional.clean(input), output) # test invalid inputs for input, errors in invalid.items(): with self.assertRaises(ValidationError) as context_manager: required.clean(input) self.assertEqual(context_manager.exception.messages, errors) with self.assertRaises(ValidationError) as context_manager: optional.clean(input) self.assertEqual(context_manager.exception.messages, errors) # test required inputs error_required = [required.error_messages[«required»]] for e in required.empty_values: with self.assertRaises(ValidationError) as context_manager: required.clean(e) self.assertEqual(context_manager.exception.messages, error_required) self.assertEqual(optional.clean(e), empty_value) # test that max_length and min_length are always accepted if issubclass(fieldclass, CharField): field_kwargs.update({«min_length»: 2, «max_length»: 20}) self.assertIsInstance(fieldclass(*field_args, **field_kwargs), fieldclass) def assertHTMLEqual(self, html1, html2, msg=None): «»» Assert that two HTML snippets are semantically the same. Whitespace in most cases is ignored, and attribute ordering is not significant. The arguments must be valid HTML. «»» dom1 = assert_and_parse_html( self, html1, msg, «First argument is not valid HTML:» ) dom2 = assert_and_parse_html( self, html2, msg, «Second argument is not valid HTML:» ) if dom1 != dom2: standardMsg = «%s != %s» % (safe_repr(dom1, True), safe_repr(dom2, True)) diff = «n« + «n«.join( difflib.ndiff( str(dom1).splitlines(), str(dom2).splitlines(), ) ) standardMsg = self._truncateMessage(standardMsg, diff) self.fail(self._formatMessage(msg, standardMsg)) def assertHTMLNotEqual(self, html1, html2, msg=None): «»»Assert that two HTML snippets are not semantically equivalent.»»» dom1 = assert_and_parse_html( self, html1, msg, «First argument is not valid HTML:» ) dom2 = assert_and_parse_html( self, html2, msg, «Second argument is not valid HTML:» ) if dom1 == dom2: standardMsg = «%s == %s» % (safe_repr(dom1, True), safe_repr(dom2, True)) self.fail(self._formatMessage(msg, standardMsg)) def assertInHTML(self, needle, haystack, count=None, msg_prefix=«»): needle = assert_and_parse_html( self, needle, None, «First argument is not valid HTML:» ) haystack = assert_and_parse_html( self, haystack, None, «Second argument is not valid HTML:» ) real_count = haystack.count(needle) if count is not None: self.assertEqual( real_count, count, msg_prefix + «Found %d instances of ‘%s’ in response (expected %d)» % (real_count, needle, count), ) else: self.assertTrue( real_count != 0, msg_prefix + «Couldn’t find ‘%s’ in response» % needle ) def assertJSONEqual(self, raw, expected_data, msg=None): «»» Assert that the JSON fragments raw and expected_data are equal. Usual JSON non-significant whitespace rules apply as the heavyweight is delegated to the json library. «»» try: data = json.loads(raw) except json.JSONDecodeError: self.fail(«First argument is not valid JSON: %r» % raw) if isinstance(expected_data, str): try: expected_data = json.loads(expected_data) except ValueError: self.fail(«Second argument is not valid JSON: %r» % expected_data) self.assertEqual(data, expected_data, msg=msg) def assertJSONNotEqual(self, raw, expected_data, msg=None): «»» Assert that the JSON fragments raw and expected_data are not equal. Usual JSON non-significant whitespace rules apply as the heavyweight is delegated to the json library. «»» try: data = json.loads(raw) except json.JSONDecodeError: self.fail(«First argument is not valid JSON: %r» % raw) if isinstance(expected_data, str): try: expected_data = json.loads(expected_data) except json.JSONDecodeError: self.fail(«Second argument is not valid JSON: %r» % expected_data) self.assertNotEqual(data, expected_data, msg=msg) def assertXMLEqual(self, xml1, xml2, msg=None): «»» Assert that two XML snippets are semantically the same. Whitespace in most cases is ignored and attribute ordering is not significant. The arguments must be valid XML. «»» try: result = compare_xml(xml1, xml2) except Exception as e: standardMsg = «First or second argument is not valid XMLn%s» % e self.fail(self._formatMessage(msg, standardMsg)) else: if not result: standardMsg = «%s != %s» % ( safe_repr(xml1, True), safe_repr(xml2, True), ) diff = «n« + «n«.join( difflib.ndiff(xml1.splitlines(), xml2.splitlines()) ) standardMsg = self._truncateMessage(standardMsg, diff) self.fail(self._formatMessage(msg, standardMsg)) def assertXMLNotEqual(self, xml1, xml2, msg=None): «»» Assert that two XML snippets are not semantically equivalent. Whitespace in most cases is ignored and attribute ordering is not significant. The arguments must be valid XML. «»» try: result = compare_xml(xml1, xml2) except Exception as e: standardMsg = «First or second argument is not valid XMLn%s» % e self.fail(self._formatMessage(msg, standardMsg)) else: if result: standardMsg = «%s == %s» % ( safe_repr(xml1, True), safe_repr(xml2, True), ) self.fail(self._formatMessage(msg, standardMsg)) class TransactionTestCase(SimpleTestCase): # Subclasses can ask for resetting of auto increment sequence before each # test case reset_sequences = False # Subclasses can enable only a subset of apps for faster tests available_apps = None # Subclasses can define fixtures which will be automatically installed. fixtures = None databases = {DEFAULT_DB_ALIAS} _disallowed_database_msg = ( «Database %(operation)s to %(alias)r are not allowed in this test. « «Add %(alias)r to %(test)s.databases to ensure proper test isolation « «and silence this failure.» ) # If transactions aren’t available, Django will serialize the database # contents into a fixture during setup and flush and reload them # during teardown (as flush does not restore data from migrations). # This can be slow; this flag allows enabling on a per-case basis. serialized_rollback = False def _pre_setup(self): «»» Perform pre-test setup: * If the class has an ‘available_apps’ attribute, restrict the app registry to these applications, then fire the post_migrate signal — it must run with the correct set of applications for the test case. * If the class has a ‘fixtures’ attribute, install those fixtures. «»» super()._pre_setup() if self.available_apps is not None: apps.set_available_apps(self.available_apps) setting_changed.send( sender=settings._wrapped.__class__, setting=«INSTALLED_APPS», value=self.available_apps, enter=True, ) for db_name in self._databases_names(include_mirrors=False): emit_post_migrate_signal(verbosity=0, interactive=False, db=db_name) try: self._fixture_setup() except Exception: if self.available_apps is not None: apps.unset_available_apps() setting_changed.send( sender=settings._wrapped.__class__, setting=«INSTALLED_APPS», value=settings.INSTALLED_APPS, enter=False, ) raise # Clear the queries_log so that it’s less likely to overflow (a single # test probably won’t execute 9K queries). If queries_log overflows, # then assertNumQueries() doesn’t work. for db_name in self._databases_names(include_mirrors=False): connections[db_name].queries_log.clear() @classmethod def _databases_names(cls, include_mirrors=True): # Only consider allowed database aliases, including mirrors or not. return [ alias for alias in connections if alias in cls.databases and ( include_mirrors or not connections[alias].settings_dict[«TEST»][«MIRROR»] ) ] def _reset_sequences(self, db_name): conn = connections[db_name] if conn.features.supports_sequence_reset: sql_list = conn.ops.sequence_reset_by_name_sql( no_style(), conn.introspection.sequence_list() ) if sql_list: with transaction.atomic(using=db_name): with conn.cursor() as cursor: for sql in sql_list: cursor.execute(sql) def _fixture_setup(self): for db_name in self._databases_names(include_mirrors=False): # Reset sequences if self.reset_sequences: self._reset_sequences(db_name) # Provide replica initial data from migrated apps, if needed. if self.serialized_rollback and hasattr( connections[db_name], «_test_serialized_contents» ): if self.available_apps is not None: apps.unset_available_apps() connections[db_name].creation.deserialize_db_from_string( connections[db_name]._test_serialized_contents ) if self.available_apps is not None: apps.set_available_apps(self.available_apps) if self.fixtures: # We have to use this slightly awkward syntax due to the fact # that we’re using *args and **kwargs together. call_command( «loaddata», *self.fixtures, **{«verbosity»: 0, «database»: db_name} ) def _should_reload_connections(self): return True def _post_teardown(self): «»» Perform post-test things: * Flush the contents of the database to leave a clean slate. If the class has an ‘available_apps’ attribute, don’t fire post_migrate. * Force-close the connection so the next test gets a clean cursor. «»» try: self._fixture_teardown() super()._post_teardown() if self._should_reload_connections(): # Some DB cursors include SQL statements as part of cursor # creation. If you have a test that does a rollback, the effect # of these statements is lost, which can affect the operation of # tests (e.g., losing a timezone setting causing objects to be # created with the wrong time). To make sure this doesn’t # happen, get a clean connection at the start of every test. for conn in connections.all(initialized_only=True): conn.close() finally: if self.available_apps is not None: apps.unset_available_apps() setting_changed.send( sender=settings._wrapped.__class__, setting=«INSTALLED_APPS», value=settings.INSTALLED_APPS, enter=False, ) def _fixture_teardown(self): # Allow TRUNCATE … CASCADE and don’t emit the post_migrate signal # when flushing only a subset of the apps for db_name in self._databases_names(include_mirrors=False): # Flush the database inhibit_post_migrate = ( self.available_apps is not None or ( # Inhibit the post_migrate signal when using serialized # rollback to avoid trying to recreate the serialized data. self.serialized_rollback and hasattr(connections[db_name], «_test_serialized_contents») ) ) call_command( «flush», verbosity=0, interactive=False, database=db_name, reset_sequences=False, allow_cascade=self.available_apps is not None, inhibit_post_migrate=inhibit_post_migrate, ) # RemovedInDjango51Warning. def assertQuerysetEqual(self, *args, **kw): warnings.warn( «assertQuerysetEqual() is deprecated in favor of assertQuerySetEqual().», category=RemovedInDjango51Warning, stacklevel=2, ) return self.assertQuerySetEqual(*args, **kw) def assertQuerySetEqual(self, qs, values, transform=None, ordered=True, msg=None): values = list(values) items = qs if transform is not None: items = map(transform, items) if not ordered: return self.assertDictEqual(Counter(items), Counter(values), msg=msg) # For example qs.iterator() could be passed as qs, but it does not # have ‘ordered’ attribute. if len(values) > 1 and hasattr(qs, «ordered») and not qs.ordered: raise ValueError( «Trying to compare non-ordered queryset against more than one « «ordered value.» ) return self.assertEqual(list(items), values, msg=msg) def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs): conn = connections[using] context = _AssertNumQueriesContext(self, num, conn) if func is None: return context with context: func(*args, **kwargs) def connections_support_transactions(aliases=None): «»» Return whether or not all (or specified) connections support transactions. «»» conns = ( connections.all() if aliases is None else (connections[alias] for alias in aliases) ) return all(conn.features.supports_transactions for conn in conns) class TestData: «»» Descriptor to provide TestCase instance isolation for attributes assigned during the setUpTestData() phase. Allow safe alteration of objects assigned in setUpTestData() by test methods by exposing deep copies instead of the original objects. Objects are deep copied using a memo kept on the test case instance in order to maintain their original relationships. «»» memo_attr = «_testdata_memo» def __init__(self, name, data): self.name = name self.data = data def get_memo(self, testcase): try: memo = getattr(testcase, self.memo_attr) except AttributeError: memo = {} setattr(testcase, self.memo_attr, memo) return memo def __get__(self, instance, owner): if instance is None: return self.data memo = self.get_memo(instance) data = deepcopy(self.data, memo) setattr(instance, self.name, data) return data def __repr__(self): return «<TestData: name=%r, data=%r>» % (self.name, self.data) class TestCase(TransactionTestCase): «»» Similar to TransactionTestCase, but use `transaction.atomic()` to achieve test isolation. In most situations, TestCase should be preferred to TransactionTestCase as it allows faster execution. However, there are some situations where using TransactionTestCase might be necessary (e.g. testing some transactional behavior). On database backends with no transaction support, TestCase behaves as TransactionTestCase. «»» @classmethod def _enter_atomics(cls): «»»Open atomic blocks for multiple databases.»»» atomics = {} for db_name in cls._databases_names(): atomic = transaction.atomic(using=db_name) atomic._from_testcase = True atomic.__enter__() atomics[db_name] = atomic return atomics @classmethod def _rollback_atomics(cls, atomics): «»»Rollback atomic blocks opened by the previous method.»»» for db_name in reversed(cls._databases_names()): transaction.set_rollback(True, using=db_name) atomics[db_name].__exit__(None, None, None) @classmethod def _databases_support_transactions(cls): return connections_support_transactions(cls.databases) @classmethod def setUpClass(cls): super().setUpClass() if not cls._databases_support_transactions(): return cls.cls_atomics = cls._enter_atomics() if cls.fixtures: for db_name in cls._databases_names(include_mirrors=False): try: call_command( «loaddata», *cls.fixtures, **{«verbosity»: 0, «database»: db_name}, ) except Exception: cls._rollback_atomics(cls.cls_atomics) raise pre_attrs = cls.__dict__.copy() try: cls.setUpTestData() except Exception: cls._rollback_atomics(cls.cls_atomics) raise for name, value in cls.__dict__.items(): if value is not pre_attrs.get(name): setattr(cls, name, TestData(name, value)) @classmethod def tearDownClass(cls): if cls._databases_support_transactions(): cls._rollback_atomics(cls.cls_atomics) for conn in connections.all(initialized_only=True): conn.close() super().tearDownClass() @classmethod def setUpTestData(cls): «»»Load initial data for the TestCase.»»» pass def _should_reload_connections(self): if self._databases_support_transactions(): return False return super()._should_reload_connections() def _fixture_setup(self): if not self._databases_support_transactions(): # If the backend does not support transactions, we should reload # class data before each test self.setUpTestData() return super()._fixture_setup() if self.reset_sequences: raise TypeError(«reset_sequences cannot be used on TestCase instances») self.atomics = self._enter_atomics() def _fixture_teardown(self): if not self._databases_support_transactions(): return super()._fixture_teardown() try: for db_name in reversed(self._databases_names()): if self._should_check_constraints(connections[db_name]): connections[db_name].check_constraints() finally: self._rollback_atomics(self.atomics) def _should_check_constraints(self, connection): return ( connection.features.can_defer_constraint_checks and not connection.needs_rollback and connection.is_usable() ) @classmethod @contextmanager def captureOnCommitCallbacks(cls, *, using=DEFAULT_DB_ALIAS, execute=False): «»»Context manager to capture transaction.on_commit() callbacks.»»» callbacks = [] start_count = len(connections[using].run_on_commit) try: yield callbacks finally: while True: callback_count = len(connections[using].run_on_commit) for _, callback, robust in connections[using].run_on_commit[ start_count: ]: callbacks.append(callback) if execute: if robust: try: callback() except Exception as e: logger.error( f»Error calling {callback.__qualname__} in « f»on_commit() (%s).», e, exc_info=True, ) else: callback() if callback_count == len(connections[using].run_on_commit): break start_count = callback_count class CheckCondition: «»»Descriptor class for deferred condition checking.»»» def __init__(self, *conditions): self.conditions = conditions def add_condition(self, condition, reason): return self.__class__(*self.conditions, (condition, reason)) def __get__(self, instance, cls=None): # Trigger access for all bases. if any(getattr(base, «__unittest_skip__», False) for base in cls.__bases__): return True for condition, reason in self.conditions: if condition(): # Override this descriptor’s value and set the skip reason. cls.__unittest_skip__ = True cls.__unittest_skip_why__ = reason return True return False def _deferredSkip(condition, reason, name): def decorator(test_func): nonlocal condition if not ( isinstance(test_func, type) and issubclass(test_func, unittest.TestCase) ): @wraps(test_func) def skip_wrapper(*args, **kwargs): if ( args and isinstance(args[0], unittest.TestCase) and connection.alias not in getattr(args[0], «databases», {}) ): raise ValueError( «%s cannot be used on %s as %s doesn’t allow queries « «against the %r database.» % ( name, args[0], args[0].__class__.__qualname__, connection.alias, ) ) if condition(): raise unittest.SkipTest(reason) return test_func(*args, **kwargs) test_item = skip_wrapper else: # Assume a class is decorated test_item = test_func databases = getattr(test_item, «databases», None) if not databases or connection.alias not in databases: # Defer raising to allow importing test class’s module. def condition(): raise ValueError( «%s cannot be used on %s as it doesn’t allow queries « «against the ‘%s’ database.» % ( name, test_item, connection.alias, ) ) # Retrieve the possibly existing value from the class’s dict to # avoid triggering the descriptor. skip = test_func.__dict__.get(«__unittest_skip__») if isinstance(skip, CheckCondition): test_item.__unittest_skip__ = skip.add_condition(condition, reason) elif skip is not True: test_item.__unittest_skip__ = CheckCondition((condition, reason)) return test_item return decorator def skipIfDBFeature(*features): «»»Skip a test if a database has at least one of the named features.»»» return _deferredSkip( lambda: any( getattr(connection.features, feature, False) for feature in features ), «Database has feature(s) %s» % «, «.join(features), «skipIfDBFeature», ) def skipUnlessDBFeature(*features): «»»Skip a test unless a database has all the named features.»»» return _deferredSkip( lambda: not all( getattr(connection.features, feature, False) for feature in features ), «Database doesn’t support feature(s): %s» % «, «.join(features), «skipUnlessDBFeature», ) def skipUnlessAnyDBFeature(*features): «»»Skip a test unless a database has any of the named features.»»» return _deferredSkip( lambda: not any( getattr(connection.features, feature, False) for feature in features ), «Database doesn’t support any of the feature(s): %s» % «, «.join(features), «skipUnlessAnyDBFeature», ) class QuietWSGIRequestHandler(WSGIRequestHandler): «»» A WSGIRequestHandler that doesn’t log to standard output any of the requests received, so as to not clutter the test result output. «»» def log_message(*args): pass class FSFilesHandler(WSGIHandler): «»» WSGI middleware that intercepts calls to a directory, as defined by one of the *_ROOT settings, and serves those files, publishing them under *_URL. «»» def __init__(self, application): self.application = application self.base_url = urlparse(self.get_base_url()) super().__init__() def _should_handle(self, path): «»» Check if the path should be handled. Ignore the path if: * the host is provided as part of the base_url * the request’s path isn’t under the media path (or equal) «»» return path.startswith(self.base_url[2]) and not self.base_url[1] def file_path(self, url): «»»Return the relative path to the file on disk for the given URL.»»» relative_url = url.removeprefix(self.base_url[2]) return url2pathname(relative_url) def get_response(self, request): from django.http import Http404 if self._should_handle(request.path): try: return self.serve(request) except Http404: pass return super().get_response(request) def serve(self, request): os_rel_path = self.file_path(request.path) os_rel_path = posixpath.normpath(unquote(os_rel_path)) # Emulate behavior of django.contrib.staticfiles.views.serve() when it # invokes staticfiles’ finders functionality. # TODO: Modify if/when that internal API is refactored final_rel_path = os_rel_path.replace(«\«, «/»).lstrip(«/») return serve(request, final_rel_path, document_root=self.get_base_dir()) def __call__(self, environ, start_response): if not self._should_handle(get_path_info(environ)): return self.application(environ, start_response) return super().__call__(environ, start_response) class _StaticFilesHandler(FSFilesHandler): «»» Handler for serving static files. A private class that is meant to be used solely as a convenience by LiveServerThread. «»» def get_base_dir(self): return settings.STATIC_ROOT def get_base_url(self): return settings.STATIC_URL class _MediaFilesHandler(FSFilesHandler): «»» Handler for serving the media files. A private class that is meant to be used solely as a convenience by LiveServerThread. «»» def get_base_dir(self): return settings.MEDIA_ROOT def get_base_url(self): return settings.MEDIA_URL class LiveServerThread(threading.Thread): «»»Thread for running a live HTTP server while the tests are running.»»» server_class = ThreadedWSGIServer def __init__(self, host, static_handler, connections_override=None, port=0): self.host = host self.port = port self.is_ready = threading.Event() self.error = None self.static_handler = static_handler self.connections_override = connections_override super().__init__() def run(self): «»» Set up the live server and databases, and then loop over handling HTTP requests. «»» if self.connections_override: # Override this thread’s database connections with the ones # provided by the main thread. for alias, conn in self.connections_override.items(): connections[alias] = conn try: # Create the handler for serving static and media files handler = self.static_handler(_MediaFilesHandler(WSGIHandler())) self.httpd = self._create_server( connections_override=self.connections_override, ) # If binding to port zero, assign the port allocated by the OS. if self.port == 0: self.port = self.httpd.server_address[1] self.httpd.set_app(handler) self.is_ready.set() self.httpd.serve_forever() except Exception as e: self.error = e self.is_ready.set() finally: connections.close_all() def _create_server(self, connections_override=None): return self.server_class( (self.host, self.port), QuietWSGIRequestHandler, allow_reuse_address=False, connections_override=connections_override, ) def terminate(self): if hasattr(self, «httpd»): # Stop the WSGI server self.httpd.shutdown() self.httpd.server_close() self.join() class LiveServerTestCase(TransactionTestCase): «»» Do basically the same as TransactionTestCase but also launch a live HTTP server in a separate thread so that the tests may use another testing framework, such as Selenium for example, instead of the built-in dummy client. It inherits from TransactionTestCase instead of TestCase because the threads don’t share the same transactions (unless if using in-memory sqlite) and each thread needs to commit all their transactions so that the other thread can see the changes. «»» host = «localhost» port = 0 server_thread_class = LiveServerThread static_handler = _StaticFilesHandler @classproperty def live_server_url(cls): return «http://%s:%s» % (cls.host, cls.server_thread.port) @classproperty def allowed_host(cls): return cls.host @classmethod def _make_connections_override(cls): connections_override = {} for conn in connections.all(): # If using in-memory sqlite databases, pass the connections to # the server thread. if conn.vendor == «sqlite» and conn.is_in_memory_db(): connections_override[conn.alias] = conn return connections_override @classmethod def setUpClass(cls): super().setUpClass() cls._live_server_modified_settings = modify_settings( ALLOWED_HOSTS={«append»: cls.allowed_host}, ) cls._live_server_modified_settings.enable() cls.addClassCleanup(cls._live_server_modified_settings.disable) cls._start_server_thread() @classmethod def _start_server_thread(cls): connections_override = cls._make_connections_override() for conn in connections_override.values(): # Explicitly enable thread-shareability for this connection. conn.inc_thread_sharing() cls.server_thread = cls._create_server_thread(connections_override) cls.server_thread.daemon = True cls.server_thread.start() cls.addClassCleanup(cls._terminate_thread) # Wait for the live server to be ready cls.server_thread.is_ready.wait() if cls.server_thread.error: raise cls.server_thread.error @classmethod def _create_server_thread(cls, connections_override): return cls.server_thread_class( cls.host, cls.static_handler, connections_override=connections_override, port=cls.port, ) @classmethod def _terminate_thread(cls): # Terminate the live server’s thread. cls.server_thread.terminate() # Restore shared connections’ non-shareability. for conn in cls.server_thread.connections_override.values(): conn.dec_thread_sharing() class SerializeMixin: «»» Enforce serialization of TestCases that share a common resource. Define a common ‘lockfile’ for each set of TestCases to serialize. This file must exist on the filesystem. Place it early in the MRO in order to isolate setUpClass()/tearDownClass(). «»» lockfile = None def __init_subclass__(cls, /, **kwargs): super().__init_subclass__(**kwargs) if cls.lockfile is None: raise ValueError( «{}.lockfile isn’t set. Set it to a unique value « «in the base class.».format(cls.__name__) ) @classmethod def setUpClass(cls): cls._lockfile = open(cls.lockfile) cls.addClassCleanup(cls._lockfile.close) locks.lock(cls._lockfile, locks.LOCK_EX) super().setUpClass()

Инструменты для тестирования¶

Django предоставляет небольшой набор инструментов, которые могут пригодиться при написании тестов.

Тестовый клиент¶

Тестовый клиент — это класс Python, который действует как фиктивный веб-браузер, позволяя вам тестировать ваши представления и взаимодействовать с вашим Django-приложением программно.

С помощью тестового клиента можно выполнять следующие действия:

  • Моделируйте запросы GET и POST на URL и наблюдайте за ответом — все, от низкоуровневого HTTP (заголовки результатов и коды состояния) до содержимого страницы.
  • Посмотрите цепочку перенаправлений (если таковые имеются) и проверьте URL и код состояния на каждом этапе.
  • Проверьте, что заданный запрос отображается заданным шаблоном Django, с контекстом шаблона, содержащим определенные значения.

Обратите внимание, что тестовый клиент не предназначен для замены Selenium или других «внутрибраузерных» фреймворков. Тестовый клиент Django имеет другую направленность. Вкратце:

  • Используйте тестовый клиент Django, чтобы убедиться, что отображается правильный шаблон и что шаблону передаются правильные контекстные данные.
  • Используйте RequestFactory для тестирования функций представления напрямую, минуя уровни маршрутизации и промежуточного ПО.
  • Используйте внутрибраузерные фреймворки, такие как Selenium для тестирования рендеринга HTML и поведения веб-страниц, а именно функциональности JavaScript. Django также предоставляет специальную поддержку для этих фреймворков; подробнее см. раздел LiveServerTestCase.

A comprehensive test suite should use a combination of all of these test types.

Обзор и небольшой пример¶

Чтобы использовать тестовый клиент, инстанцируйте django.test.Client и получите веб-страницы:

>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'

Как следует из этого примера, вы можете инстанцировать Client из сеанса интерактивного интерпретатора Python.

Обратите внимание на несколько важных моментов в работе тестового клиента:

  • Тестовый клиент не требует, чтобы веб-сервер был запущен. На самом деле, он будет прекрасно работать и без веб-сервера! Это потому, что он избегает накладных расходов HTTP и работает напрямую с фреймворком Django. Это помогает быстро запускать модульные тесты.

  • При получении страниц не забывайте указывать путь URL, а не весь домен. Например, правильно будет:

    Это неверно:

    >>> c.get('https://www.example.com/login/')
    

    Тестовый клиент не способен получать веб-страницы, которые не работают с вашим проектом Django. Если вам нужно получить другие веб-страницы, используйте модуль стандартной библиотеки Python, такой как urllib.

  • Для разрешения URL-адресов тестовый клиент использует тот URLconf, на который указывает ваша настройка ROOT_URLCONF.

  • Хотя приведенный выше пример будет работать в интерактивном интерпретаторе Python, некоторые функции тестового клиента, в частности, связанные с шаблонами, доступны только во время выполнения тестов.

    Причина этого в том, что программа запуска тестов Django выполняет немного черной магии, чтобы определить, какой шаблон был загружен данным представлением. Эта черная магия (по сути, исправление системы шаблонов Django в памяти) происходит только во время выполнения теста.

  • По умолчанию тестовый клиент отключает любые проверки CSRF, выполняемые вашим сайтом.

    Если по какой-то причине вы хотите, чтобы тестовый клиент выполнял проверку CSRF, вы можете создать экземпляр тестового клиента, который будет выполнять проверку CSRF. Для этого передайте аргумент enforce_csrf_checks при создании клиента:

    >>> from django.test import Client
    >>> csrf_client = Client(enforce_csrf_checks=True)
    

Выполнение запросов¶

Используйте класс django.test.Client для выполнения запросов.

class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, **defaults)[исходный код]

Он не требует аргументов во время построения. Однако вы можете использовать ключевые аргументы для указания некоторых заголовков по умолчанию. Например, эта программа будет отправлять HTTP-заголовок User-Agent в каждом запросе:

>>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')

Значения из аргументов ключевого слова extra, переданных в get(), post() и т.д., имеют приоритет над значениями по умолчанию, переданными в конструктор класса.

Аргумент enforce_csrf_checks можно использовать для проверки защиты от CSRF (см. выше).

Аргумент raise_request_exception позволяет контролировать, должны ли исключения, возникающие во время запроса, также возникать в тесте. По умолчанию установлено значение True.

Аргумент json_encoder позволяет установить пользовательский JSON-кодер для сериализации JSON, описанной в post().

Когда у вас есть экземпляр Client, вы можете вызвать любой из следующих методов:

get(path, data=None, follow=False, secure=False, **extra)[исходный код]

Выполняет GET-запрос на предоставленный path и возвращает объект Response, который документирован ниже.

Пары ключ-значение в словаре data используются для создания полезной нагрузки данных GET. Например:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7})

…приведет к оценке GET-запроса, эквивалентного:

/customers/details/?name=fred&age=7

Параметр аргументов с ключевым словом extra может использоваться для указания заголовков, которые должны быть отправлены в запросе. Например:

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
...       HTTP_ACCEPT='application/json')

…отправит HTTP-заголовок HTTP_ACCEPT в представление деталей, что является хорошим способом тестирования путей кода, использующих метод django.http.HttpRequest.accepts().

Спецификация CGI

Заголовки, передаваемые через **extra, должны соответствовать спецификации CGI. Например, эмуляция другого заголовка «Host», отправленного в HTTP-запросе от браузера к серверу, должна быть передана как HTTP_HOST.

Если у вас уже есть аргументы GET в URL-кодировке, вы можете использовать эту кодировку вместо аргумента data. Например, предыдущий GET-запрос можно сформулировать так:

>>> c = Client()
>>> c.get('/customers/details/?name=fred&age=7')

Если вы предоставляете URL с закодированными данными GET и аргументом data, аргумент data будет иметь приоритет.

Если вы установите follow в True, клиент будет следовать любым перенаправлениям, а в объекте ответа будет установлен атрибут redirect_chain, содержащий кортежи промежуточных адресов и кодов состояния.

Если у вас есть URL /redirect_me/, который перенаправляется на /next/, который перенаправляется на /final/, вот что вы увидите:

>>> response = c.get('/redirect_me/', follow=True)
>>> response.redirect_chain
[('http://testserver/next/', 302), ('http://testserver/final/', 302)]

Если вы установите secure в True, клиент будет эмулировать запрос HTTPS.

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra)[исходный код]

Выполняет POST-запрос на предоставленный path и возвращает объект Response, который документирован ниже.

Пары ключ-значение в словаре data используются для отправки данных POST. Например:

>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})

…приведет к оценке POST-запроса к этому URL:

…с этими данными POST:

Если вы предоставите content_type в качестве application/json, data будет сериализован с помощью json.dumps(), если это dict, список или кортеж. По умолчанию сериализация выполняется с помощью DjangoJSONEncoder, и ее можно переопределить, предоставив аргумент json_encoder для Client. Эта сериализация также происходит для запросов put(), patch() и delete().

Если вы предоставите любой другой content_type (например, text/xml для полезной нагрузки XML), содержимое data будет отправлено как есть в POST-запросе, используя content_type в заголовке HTTP Content-Type.

Если вы не укажете значение для content_type, значения в data будут переданы с типом содержимого multipart/form-data. В этом случае пары ключ-значение в data будут закодированы как многокомпонентное сообщение и использованы для создания полезной нагрузки данных POST.

Чтобы отправить несколько значений для заданного ключа — например, чтобы указать выбранные значения для поля <select multiple> — предоставьте значения в виде списка или кортежа для требуемого ключа. Например, значение data представит три выбранных значения для поля с именем choices:

{'choices': ('a', 'b', 'd')}

Отправка файлов — это особый случай. Чтобы отправить файл, достаточно указать имя поля файла в качестве ключа, а в качестве значения — хэндл файла, который вы хотите загрузить. Например, если ваша форма имеет поля name и attachment, последнее — FileField:

>>> c = Client()
>>> with open('wishlist.doc', 'rb') as fp:
...     c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

Вы также можете предоставить любой файлоподобный объект (например, StringIO или BytesIO) в качестве дескриптора файла. Если вы загружаете на ImageField, то объект должен иметь name атрибут, который проходит validate_image_file_extension валидатор. Например:

>>> from io import BytesIO
>>> img = BytesIO(
...     b"GIF89ax01x00x01x00x00x00x00!xf9x04x01x00x00x00"
...     b"x00,x00x00x00x00x01x00x01x00x00x02x01x00x00"
... )
>>> img.name = "myimage.gif"

Обратите внимание, что если вы хотите использовать один и тот же дескриптор файла для нескольких вызовов post(), то вам нужно будет вручную сбрасывать указатель файла между вызовами. Самый простой способ сделать это — вручную закрыть файл после того, как он был предоставлен post(), как показано выше.

Вы также должны убедиться, что файл открыт таким образом, чтобы данные можно было прочитать. Если ваш файл содержит двоичные данные, например, изображение, это означает, что вам нужно открыть файл в режиме rb (чтение двоичных данных).

Аргумент extra действует так же, как и для Client.get().

Если URL, который вы запрашиваете с помощью POST, содержит закодированные параметры, эти параметры будут доступны в данных request.GET. Например, если вы сделаете запрос:

>>> c.post('/login/?visitor=true', {'name': 'fred', 'passwd': 'secret'})

… представление, обрабатывающее этот запрос, может запросить request.POST, чтобы получить имя пользователя и пароль, и может запросить request.GET, чтобы определить, был ли пользователь посетителем.

Если вы установите follow в True, клиент будет следовать любым перенаправлениям, а в объекте ответа будет установлен атрибут redirect_chain, содержащий кортежи промежуточных адресов и кодов состояния.

Если вы установите secure в True, клиент будет эмулировать запрос HTTPS.

head(path, data=None, follow=False, secure=False, **extra)[исходный код]

Выполняет запрос HEAD на предоставленном path и возвращает объект Response. Этот метод работает так же, как Client.get(), включая аргументы follow, secure и extra, за исключением того, что он не возвращает тело сообщения.

options(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra)[исходный код]

Выполняет запрос OPTIONS на предоставленный path и возвращает объект Response. Используется для тестирования RESTful интерфейсов.

Когда предоставляется data, он используется в качестве тела запроса, а заголовок Content-Type устанавливается в content_type.

Аргументы follow, secure и extra действуют так же, как и для Client.get().

put(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra)[исходный код]

Выполняет запрос PUT на предоставленный path и возвращает объект Response. Полезно для тестирования RESTful интерфейсов.

Когда предоставляется data, он используется в качестве тела запроса, а заголовок Content-Type устанавливается в content_type.

Аргументы follow, secure и extra действуют так же, как и для Client.get().

patch(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra)[исходный код]

Выполняет запрос PATCH на предоставленный path и возвращает объект Response. Полезно для тестирования RESTful интерфейсов.

Аргументы follow, secure и extra действуют так же, как и для Client.get().

delete(path, data=», content_type=‘application/octet-stream’, follow=False, secure=False, **extra)[исходный код]

Делает запрос DELETE на предоставленный path и возвращает объект Response. Полезно для тестирования RESTful интерфейсов.

Когда предоставляется data, он используется в качестве тела запроса, а заголовок Content-Type устанавливается в content_type.

Аргументы follow, secure и extra действуют так же, как и для Client.get().

trace(path, follow=False, secure=False, **extra)[исходный код]

Делает запрос TRACE на предоставленный path и возвращает объект Response. Полезен для имитации диагностических зондов.

В отличие от других методов запроса, data не предоставляется в качестве параметра ключевого слова, чтобы соответствовать RFC 7231#section-4.3.8, который предписывает, что запросы TRACE не должны иметь тела.

Аргументы follow, secure и extra действуют так же, как и для Client.get().

login(**credentials

Если ваш сайт использует authentication system Django и вы имеете дело с регистрацией пользователей, вы можете использовать метод login() тестового клиента для имитации эффекта входа пользователя на сайт.

После вызова этого метода тестовый клиент будет иметь все куки и данные сессии, необходимые для прохождения любых тестов на основе входа в систему, которые могут быть частью представления.

Формат аргумента credentials зависит от того, какой authentication backend вы используете (который задается настройками AUTHENTICATION_BACKENDS). Если вы используете стандартный бэкенд аутентификации, предоставляемый Django (ModelBackend), credentials должны быть имя пользователя и пароль, предоставленные в качестве аргументов ключевых слов:

>>> c = Client()
>>> c.login(username='fred', password='secret')

# Now you can access a view that's only available to logged-in users.

Если вы используете другой бэкенд аутентификации, этот метод может потребовать другие учетные данные. Он требует те учетные данные, которые требуются для метода authenticate() вашего бэкенда.

login() возвращает True, если учетные данные были приняты и вход был успешным.

Наконец, вам нужно будет не забыть создать учетные записи пользователей, прежде чем вы сможете использовать этот метод. Как мы объяснили выше, программа запуска теста выполняется с использованием тестовой базы данных, которая по умолчанию не содержит пользователей. В результате учетные записи пользователей, действующие на вашем рабочем сайте, не будут работать в условиях тестирования. Вам нужно будет создать пользователей в рамках тестового пакета — либо вручную (используя API модели Django), либо с помощью тестового приспособления. Помните, что если вы хотите, чтобы у вашего тестового пользователя был пароль, вы не можете установить пароль пользователя, задав атрибут password напрямую — вы должны использовать функцию set_password() для хранения правильно хэшированного пароля. В качестве альтернативы вы можете использовать вспомогательный метод create_user() для создания нового пользователя с правильно хэшированным паролем.

force_login(user, backend=None

Если ваш сайт использует authentication system Django, вы можете использовать метод force_login() для имитации эффекта входа пользователя на сайт. Используйте этот метод вместо login(), когда тест требует, чтобы пользователь вошел в систему, а детали того, как пользователь вошел в систему, не важны.

В отличие от login(), этот метод пропускает этапы аутентификации и проверки: неактивным пользователям (is_active=False) разрешено входить в систему, а учетные данные пользователя предоставлять не нужно.

Атрибут пользователя backend будет установлен на значение аргумента backend (который должен быть точечной строкой пути Python), или на settings.AUTHENTICATION_BACKENDS[0], если значение не предоставлено. Функция authenticate(), вызываемая login(), обычно аннотирует пользователя следующим образом.

Этот метод быстрее, чем login(), поскольку обходятся дорогостоящие алгоритмы хэширования паролей. Кроме того, вы можете ускорить login() на using a weaker hasher while testing.

logout()¶

Если ваш сайт использует Django authentication system, метод logout() может быть использован для имитации эффекта выхода пользователя из сайта.

После вызова этого метода у тестового клиента все cookies и данные сессии будут очищены до значений по умолчанию. Последующие запросы будут выглядеть как исходящие от AnonymousUser.

Ответы на тестирование¶

Методы get() и post() оба возвращают объект Response. Этот объект Response не такой же, как объект HttpResponse, возвращаемый представлениями Django; объект тестового ответа имеет некоторые дополнительные данные, полезные для проверки тестовым кодом.

В частности, объект Response имеет следующие атрибуты:

class Response
client

Тестовый клиент, который был использован для выполнения запроса, в результате которого был получен ответ.

content

Тело ответа в виде байтовой строки. Это конечное содержимое страницы, отображаемое представлением, или любое сообщение об ошибке.

context

Экземпляр шаблона Context, который был использован для рендеринга шаблона, создавшего содержимое ответа.

Если на странице использовалось несколько шаблонов, то context будет список Context объектов, в том порядке, в котором они были отображены.

Независимо от количества шаблонов, используемых во время рендеринга, вы можете получить значения контекста с помощью оператора []. Например, контекстная переменная name может быть получена с помощью:

>>> response = client.get('/foo/')
>>> response.context['name']
'Arthur'

Не используете шаблоны Django?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой шаблонизатор, context_data может быть подходящей альтернативой для ответов с этим атрибутом.

exc_info

Кортеж из трех значений, который предоставляет информацию о необработанном исключении, если таковое имело место, которое произошло во время просмотра.

Значения (type, value, traceback), те же, что возвращает Python sys.exc_info(). Их значения следующие:

  • тип: Тип исключения.
  • значение: Экземпляр исключения.
  • traceback: Объект traceback, который содержит стек вызовов в точке, где первоначально произошло исключение.

Если исключение не произошло, то exc_info будет None.

json(**kwargs

Тело ответа, разобранное как JSON. Дополнительные аргументы в виде ключевых слов передаются в json.loads(). Например:

>>> response = client.get('/foo/')
>>> response.json()['name']
'Arthur'

Если заголовок Content-Type не "application/json", то при попытке разобрать ответ возникнет ошибка ValueError.

request

Данные запроса, которые стимулировали ответ.

wsgi_request

Экземпляр WSGIRequest, созданный обработчиком теста, который сгенерировал ответ.

status_code

HTTP-статус ответа, в виде целого числа. Полный список определенных кодов см. в IANA status code registry.

templates

Список шаблонов Template, используемых для отображения конечного содержимого, в порядке их отображения. Для каждого шаблона в списке используйте template.name, чтобы получить имя файла шаблона, если шаблон был загружен из файла. (Имя представляет собой строку, например 'admin/index.html').

Не используете шаблоны Django?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой шаблонизатор, template_name может быть подходящей альтернативой, если вам нужно только имя шаблона, используемого для рендеринга.

resolver_match

Экземпляр ResolverMatch для ответа. Вы можете использовать атрибут func, например, для проверки представления, обслужившего ответ:

# my_view here is a function based view.
self.assertEqual(response.resolver_match.func, my_view)

# Class-based views need to compare the view_class, as the
# functions generated by as_view() won't be equal.
self.assertIs(response.resolver_match.func.view_class, MyView)

Если заданный URL не найден, обращение к этому атрибуту вызовет исключение Resolver404.

Как и в случае с обычным ответом, вы также можете получить доступ к заголовкам через HttpResponse.headers. Например, можно определить тип содержимого ответа с помощью response.headers['Content-Type'].

Исключения¶

Если вы направите тестовый клиент на представление, которое вызывает исключение, и Client.raise_request_exception будет True, это исключение будет видно в тестовом примере. Затем вы можете использовать стандартный блок try ... except или assertRaises() для проверки исключений.

Единственными исключениями, которые не видны тестовому клиенту, являются Http404, PermissionDenied, SystemExit и SuspiciousOperation. Django перехватывает эти исключения внутренне и преобразует их в соответствующие коды ответов HTTP. В этих случаях вы можете проверить response.status_code в вашем тесте.

Если Client.raise_request_exception равно False, тестовый клиент вернет ответ 500, как это было бы в браузере. Ответ имеет атрибут exc_info для предоставления информации о необработанном исключении.

Постоянное состояние¶

Тестовый клиент является государственным. Если ответ возвращает cookie, то это cookie будет сохранено в тестовом клиенте и отправлено со всеми последующими запросами get() и post().

Политика истечения срока действия этих файлов cookie не соблюдается. Если вы хотите, чтобы срок действия cookie истек, либо удалите его вручную, либо создайте новый экземпляр Client (что приведет к эффективному удалению всех cookie).

Клиент теста имеет атрибуты, которые хранят постоянную информацию о состоянии. Вы можете получить доступ к этим свойствам как часть условия теста.

Client.cookies

Объект Python SimpleCookie, содержащий текущие значения всех клиентских cookies. Подробнее см. документацию модуля http.cookies.

Client.session

Словарно-подобный объект, содержащий информацию о сеансе. Подробную информацию см. в session documentation.

Чтобы изменить сессию и затем сохранить ее, ее необходимо сначала сохранить в переменной (потому что при каждом обращении к этому свойству создается новое SessionStore):

def test_something(self):
    session = self.client.session
    session['somekey'] = 'test'
    session.save()

Настройка языка¶

При тестировании приложений, поддерживающих интернационализацию и локализацию, вам может понадобиться установить язык для запроса тестового клиента. Метод для этого зависит от того, включен или нет параметр LocaleMiddleware.

Если промежуточное ПО включено, язык может быть установлен путем создания cookie с именем LANGUAGE_COOKIE_NAME и значением кода языка:

from django.conf import settings

def test_language_using_cookie(self):
    self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'fr'})
    response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

или включив в запрос HTTP-заголовок Accept-Language:

def test_language_using_header(self):
    response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fr')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

Более подробную информацию можно найти в Как Django обнаруживает языковые предпочтения.

Если промежуточное ПО не включено, активный язык может быть установлен с помощью translation.override():

from django.utils import translation

def test_language_using_override(self):
    with translation.override('fr'):
        response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

Более подробную информацию можно найти в Явная установка активного языка.

Пример¶

Ниже приведен модульный тест с использованием тестового клиента:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def setUp(self):
        # Every test needs a client.
        self.client = Client()

    def test_details(self):
        # Issue a GET request.
        response = self.client.get('/customer/details/')

        # Check that the response is 200 OK.
        self.assertEqual(response.status_code, 200)

        # Check that the rendered context contains 5 customers.
        self.assertEqual(len(response.context['customers']), 5)

Предоставленные классы тестовых примеров¶

Обычные классы модульных тестов Python расширяют базовый класс unittest.TestCase. Django предоставляет несколько расширений этого базового класса:

Hierarchy of Django unit testing classes (TestCase subclasses)

Иерархия классов модульного тестирования Django

Вы можете преобразовать обычный unittest.TestCase в любой из подклассов: измените базовый класс вашего теста с unittest.TestCase на подкласс. Все стандартные функции модульных тестов Python будут доступны, и они будут дополнены некоторыми полезными дополнениями, описанными в каждом разделе ниже.

SimpleTestCase

class SimpleTestCase[исходный код]

Подкласс unittest.TestCase, который добавляет эту функциональность:

  • Некоторые полезные утверждения, такие как:
    • Проверка того, что вызываемый объект raises a certain exception.
    • Проверка того, что вызываемый объект triggers a certain warning.
    • Тестирование поля формы rendering and error treatment.
    • Тестирование HTML responses for the presence/lack of a given fragment.
    • Проверка того, что шаблон has/hasn't been used to generate a given response content.
    • Проверка того, что два URLs равны.
    • Проверка HTTP redirect выполняется приложением.
    • Надежное тестирование двух HTML fragments на равенство/неравенство или containment.
    • Надежное тестирование двух XML fragments на равенство/неравенство.
    • Надежная проверка двух JSON fragments на равенство.
  • Возможность запускать тесты с modified settings.
  • Используя client Client.

Если ваши тесты делают какие-либо запросы к базе данных, используйте подклассы TransactionTestCase или TestCase.

SimpleTestCase.databases

SimpleTestCase запрещает запросы к базе данных по умолчанию. Это помогает избежать выполнения запросов на запись, которые повлияют на другие тесты, поскольку каждый SimpleTestCase тест не выполняется в транзакции. Если вас не беспокоит эта проблема, вы можете отключить это поведение, установив атрибут databases class в '__all__' на вашем тестовом классе.

Предупреждение

SimpleTestCase и его подклассы (например, TestCase, …) полагаются на setUpClass() и tearDownClass() для выполнения некоторой инициализации в масштабах класса (например, переопределение настроек). Если вам нужно переопределить эти методы, не забудьте вызвать реализацию super:

class MyTestCase(TestCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

    @classmethod
    def tearDownClass(cls):
        ...
        super().tearDownClass()

Не забудьте учесть поведение Python, если во время выполнения setUpClass() возникнет исключение. Если это произойдет, ни тесты в классе, ни tearDownClass() не будут выполнены. В случае django.test.TestCase произойдет утечка транзакции, созданной в super(), что приведет к различным симптомам, включая ошибку сегментации на некоторых платформах (сообщалось на macOS). Если вы хотите намеренно вызвать исключение, такое как unittest.SkipTest в setUpClass(), обязательно сделайте это до вызова super(), чтобы избежать этого.

TransactionTestCase

class TransactionTestCase[исходный код]

TransactionTestCase наследуется от SimpleTestCase, чтобы добавить некоторые специфические для базы данных возможности:

  • Сброс базы данных в известное состояние в начале каждого теста для облегчения тестирования и использования ORM.
  • База данных fixtures.
  • Тест skipping based on database backend features.
  • Остальные специализированные методы assert*.

Класс Django TestCase является более часто используемым подклассом класса TransactionTestCase, который использует средства транзакций базы данных для ускорения процесса сброса базы данных в известное состояние в начале каждого теста. Следствием этого, однако, является то, что некоторые поведения базы данных не могут быть протестированы в классе Django TestCase. Например, вы не можете проверить, что блок кода выполняется в рамках транзакции, как это требуется при использовании select_for_update(). В таких случаях следует использовать TransactionTestCase.

TransactionTestCase и TestCase идентичны, за исключением способа сброса базы данных в известное состояние и возможности для тестового кода проверить эффекты фиксации и отката:

  • Вариант TransactionTestCase сбрасывает базу данных после выполнения теста, усекая все таблицы. A TransactionTestCase может вызывать фиксацию и откат и наблюдать за влиянием этих вызовов на базу данных.
  • С другой стороны, TestCase не усекает таблицы после теста. Вместо этого он заключает тестовый код в транзакцию базы данных, которая откатывается в конце теста. Это гарантирует, что откат в конце теста восстановит базу данных в исходное состояние.

Предупреждение

TestCase, запущенный на базе данных, которая не поддерживает откат (например, MySQL с механизмом хранения MyISAM), и все экземпляры TransactionTestCase, откатятся в конце теста, удалив все данные из тестовой базы данных.

Apps will not see their data reloaded; если вам нужна эта функциональность (например, сторонние приложения должны включить ее), вы можете установить serialized_rollback = True внутри тела TestCase.

TestCase

class TestCase[исходный код]

Это самый распространенный класс для написания тестов в Django. Он наследуется от TransactionTestCase (и, соответственно, от SimpleTestCase). Если ваше приложение Django не использует базу данных, используйте SimpleTestCase.

Класс:

  • Обертывает тесты в два вложенных блока atomic(): один для всего класса и один для каждого теста. Поэтому, если вы хотите протестировать определенное поведение транзакции базы данных, используйте TransactionTestCase.
  • Проверяет отложенные ограничения базы данных в конце каждого теста.

Он также предоставляет дополнительный метод:

classmethod TestCase.setUpTestData()[исходный код]

Описанный выше блок atomic на уровне класса позволяет создавать начальные данные на уровне класса, один раз для всего TestCase. Эта техника позволяет ускорить тестирование по сравнению с использованием setUp().

Например:

from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Обратите внимание, что если тесты выполняются на базе данных без поддержки транзакций (например, MySQL с движком MyISAM), setUpTestData() будет вызываться перед каждым тестом, сводя на нет преимущества в скорости.

Объекты, назначенные атрибутам класса в setUpTestData(), должны поддерживать создание глубоких копий с помощью copy.deepcopy() для того, чтобы изолировать их от изменений, выполняемых каждым из методов тестирования.

classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)[исходный код]

Возвращает менеджер контекста, который перехватывает transaction.on_commit() обратных вызовов для данного соединения с базой данных. Он возвращает список, который содержит, при выходе из контекста, захваченные функции обратного вызова. Из этого списка вы можете сделать утверждения для обратных вызовов или вызвать их, чтобы вызвать их побочные эффекты, эмулируя фиксацию.

using — псевдоним соединения с базой данных, для которого нужно перехватить обратные вызовы.

Если execute равно True, то все обратные вызовы будут вызваны при выходе из контекстного менеджера, если не произошло исключения. Это эмулирует фиксацию после завернутого блока кода.

Например:

from django.core import mail
from django.test import TestCase


class ContactTests(TestCase):
    def test_post(self):
        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            response = self.client.post(
                '/contact/',
                {'message': 'I like your site'},
            )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(callbacks), 1)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, 'Contact Form')
        self.assertEqual(mail.outbox[0].body, 'I like your site')

Changed in Django 4.0:

В старых версиях новые обратные вызовы, добавленные во время выполнения обратных вызовов transaction.on_commit(), не перехватывались.

LiveServerTestCase

class LiveServerTestCase[исходный код]

LiveServerTestCase делает практически то же самое, что и TransactionTestCase с одной дополнительной функцией: он запускает живой сервер Django в фоновом режиме при установке и выключает его при завершении работы. Это позволяет использовать клиенты автоматизированного тестирования, отличные от Django dummy client, такие как, например, клиент Selenium, для выполнения серии функциональных тестов внутри браузера и имитации действий реального пользователя.

Живой сервер слушает на localhost и привязывается к порту 0, который использует свободный порт, назначенный операционной системой. Во время тестов доступ к URL сервера можно получить с помощью self.live_server_url.

Чтобы продемонстрировать, как использовать LiveServerTestCase, давайте напишем тест Selenium. Прежде всего, вам необходимо установить selenium package в ваш путь к Python:

/

$ python -m pip install selenium
...> py -m pip install selenium

Затем добавьте тест на основе LiveServerTestCase в модуль тестов вашего приложения (например: myapp/tests.py). В этом примере мы предположим, что вы используете приложение staticfiles и хотите, чтобы статические файлы обслуживались во время выполнения ваших тестов аналогично тому, что мы получаем во время разработки с помощью DEBUG=True, т.е. без необходимости собирать их с помощью collectstatic. Мы будем использовать подкласс StaticLiveServerTestCase, который обеспечивает эту функциональность. Замените его на django.test.LiveServerTestCase, если вам это не нужно.

Код для этого теста может выглядеть следующим образом:

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver

class MySeleniumTests(StaticLiveServerTestCase):
    fixtures = ['user-data.json']

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
        username_input = self.selenium.find_element(By.NAME, "username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element(By.NAME, "password")
        password_input.send_keys('secret')
        self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()

Наконец, вы можете запустить тест следующим образом:

/

$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...> manage.py test myapp.tests.MySeleniumTests.test_login

В этом примере автоматически откроется Firefox, затем перейдите на страницу входа, введите учетные данные и нажмите кнопку «Войти». Selenium предлагает другие драйверы на случай, если у вас не установлен Firefox или вы хотите использовать другой браузер. Приведенный выше пример — лишь малая часть того, что может делать клиент Selenium; для получения более подробной информации ознакомьтесь с full reference.

Примечание

При использовании базы данных in-memory SQLite для запуска тестов одно и то же соединение с базой данных будет использоваться параллельно двумя потоками: потоком, в котором запускается живой сервер, и потоком, в котором запускается тестовый пример. Важно предотвратить одновременные запросы к базе данных через это общее соединение двумя потоками, так как это может привести к случайному сбою тестов. Поэтому вам нужно убедиться, что эти два потока не обращаются к базе данных в одно и то же время. В частности, это означает, что в некоторых случаях (например, сразу после нажатия на ссылку или отправки формы) вам может понадобиться проверить, что Selenium получил ответ и что следующая страница загружена, прежде чем приступать к дальнейшему выполнению теста. Сделать это можно, например, заставив Selenium ждать, пока в ответе не будет найден HTML-тег <body> (требуется Selenium > 2.13):

def test_login(self):
    from selenium.webdriver.support.wait import WebDriverWait
    timeout = 2
    ...
    self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
    # Wait until the response is received
    WebDriverWait(self.selenium, timeout).until(
        lambda driver: driver.find_element(By.TAG_NAME, 'body'))

Сложность здесь в том, что на самом деле не существует такого понятия, как «загрузка страницы», особенно в современных веб-приложениях, которые генерируют HTML динамически после того, как сервер создаст исходный документ. Поэтому проверка наличия <body> в ответе не всегда подходит для всех случаев использования. Пожалуйста, обратитесь к Selenium FAQ и Selenium documentation для получения дополнительной информации.

Особенности тестовых случаев¶

Тестовый клиент по умолчанию¶

SimpleTestCase.client

Каждый тестовый пример в экземпляре django.test.*TestCase имеет доступ к экземпляру тестового клиента Django. Доступ к этому клиенту можно получить по адресу self.client. Этот клиент создается заново для каждого теста, поэтому вам не нужно беспокоиться о том, что состояние (например, cookies) будет переноситься из одного теста в другой.

Это означает, что вместо инстанцирования Client в каждом test:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def test_details(self):
        client = Client()
        response = client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        client = Client()
        response = client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

…вы можете ссылаться на self.client, например, так:

from django.test import TestCase

class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

Настройка клиента тестирования¶

SimpleTestCase.client_class

Если вы хотите использовать другой Client класс (например, подкласс с настроенным поведением), используйте атрибут client_class класса:

from django.test import Client, TestCase

class MyTestClient(Client):
    # Specialized methods for your environment
    ...

class MyTest(TestCase):
    client_class = MyTestClient

    def test_my_stuff(self):
        # Here self.client is an instance of MyTestClient...
        call_some_test_code()

Загрузка приспособлений¶

TransactionTestCase.fixtures

Тестовый пример для сайта с базой данных не имеет особого смысла, если в базе данных нет данных. Тесты более читабельны, и их удобнее поддерживать, если создавать объекты с помощью ORM, например, в TestCase.setUpTestData(), однако можно использовать и фикстуры.

Фикстура — это набор данных, которые Django умеет импортировать в базу данных. Например, если на вашем сайте есть учетные записи пользователей, вы можете создать фикстуру из поддельных учетных записей пользователей, чтобы заполнить базу данных во время тестирования.

Самым простым способом создания приспособления является использование команды manage.py dumpdata. Это предполагает, что у вас уже есть некоторые данные в вашей базе данных. Для получения более подробной информации см. команду dumpdata documentation.

После создания фикстуры и размещения ее в каталоге fixtures в одном из ваших INSTALLED_APPS, вы можете использовать ее в ваших модульных тестах, указав атрибут fixtures class в вашем django.test.TestCase subclass:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    fixtures = ['mammals.json', 'birds']

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

Вот что конкретно произойдет:

  • В начале каждого теста, перед выполнением setUp(), Django будет промывать базу данных, возвращая ее в состояние, в котором она находилась непосредственно после вызова migrate.
  • Затем устанавливаются все названные фикстуры. В этом примере Django установит любой JSON фикс с именем mammals, а затем любой фикс с именем birds. Более подробно об определении и установке фикстур смотрите в документации loaddata.

По соображениям производительности TestCase загружает фикстуры один раз для всего класса тестов, перед setUpTestData(), а не перед каждым тестом, и использует транзакции для очистки базы данных перед каждым тестом. В любом случае, вы можете быть уверены, что на результат теста не повлияет другой тест или порядок его выполнения.

По умолчанию приспособления загружаются только в базу данных default. Если вы используете несколько баз данных и установили значение TransactionTestCase.databases, приспособления будут загружены во все указанные базы данных.

Конфигурация URLconf¶

Если ваше приложение предоставляет представления, вы можете включить тесты, которые используют тестовый клиент для выполнения этих представлений. Однако конечный пользователь может свободно развернуть представления в вашем приложении на любом URL по своему выбору. Это означает, что ваши тесты не могут полагаться на то, что ваши представления будут доступны на определенном URL. Украсьте свой тестовый класс или метод теста символом @override_settings(ROOT_URLCONF=...) для конфигурации URLconf.

Поддержка нескольких баз данных¶

TransactionTestCase.databases

Django устанавливает тестовую базу данных, соответствующую каждой базе данных, которая определена в определении DATABASES в ваших настройках и на которую ссылается хотя бы один тест через databases.

Однако большая часть времени, затрачиваемого на выполнение Django TestCase, приходится на вызов flush, который гарантирует, что в начале каждого теста у вас будет чистая база данных. Если у вас несколько баз данных, то требуется несколько промывок (по одной для каждой базы данных), что может отнимать много времени — особенно если вашим тестам не нужно тестировать работу с несколькими базами данных.

В качестве оптимизации, Django промывает только базу данных default в начале каждого запуска теста. Если ваша установка содержит несколько баз данных, и у вас есть тест, который требует очистки каждой базы данных, вы можете использовать атрибут databases в наборе тестов, чтобы запросить очистку дополнительных баз данных.

Например:

class TestMyViews(TransactionTestCase):
    databases = {'default', 'other'}

    def test_index_page_view(self):
        call_some_test_code()

В этом тестовом примере перед выполнением default и other будут промыты тестовые базы данных test_index_page_view. Вы также можете использовать '__all__', чтобы указать, что все тестовые базы данных должны быть промыты.

Флаг databases также контролирует, в какие базы данных загружается TransactionTestCase.fixtures. По умолчанию приспособления загружаются только в базу данных default.

Запросы к базам данных, не входящим в databases, будут выдавать ошибки утверждения, чтобы предотвратить утечку состояния между тестами.

TestCase.databases

По умолчанию, только база данных default будет обернута в транзакцию во время выполнения TestCase, а попытки запросить другие базы данных приведут к ошибкам утверждения, чтобы предотвратить утечку состояния между тестами.

Используйте атрибут databases class на тестовом классе, чтобы запросить обертывание транзакций против не«default« баз данных.

Например:

class OtherDBTests(TestCase):
    databases = {'other'}

    def test_other_db_query(self):
        ...

Этот тест разрешит запросы только к базе данных other. Как и для SimpleTestCase.databases и TransactionTestCase.databases, константа '__all__' может быть использована для указания того, что тест должен разрешить запросы ко всем базам данных.

Переопределение настроек¶

Предупреждение

Используйте приведенные ниже функции для временного изменения значений параметров в тестах. Не манипулируйте django.conf.settings напрямую, так как Django не восстановит исходные значения после таких манипуляций.

SimpleTestCase.settings()[исходный код]

Для целей тестирования часто бывает полезно временно изменить настройки и вернуться к исходному значению после выполнения кода тестирования. Для этого случая Django предоставляет стандартный менеджер контекстов Python (см. PEP 343) под названием settings(), который можно использовать следующим образом:

from django.test import TestCase

class LoginTestCase(TestCase):

    def test_login(self):

        # First check for the default behavior
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/')

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL='/other/login/'):
            response = self.client.get('/sekrit/')
            self.assertRedirects(response, '/other/login/?next=/sekrit/')

Этот пример отменит установку LOGIN_URL для кода в блоке with и после этого сбросит его значение в предыдущее состояние.

SimpleTestCase.modify_settings()[исходный код]

Переопределение параметров, содержащих список значений, может оказаться громоздким. На практике часто бывает достаточно добавить или удалить значения. Django предоставляет контекстный менеджер modify_settings() для более простого изменения настроек:

from django.test import TestCase

class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        with self.modify_settings(MIDDLEWARE={
            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
            'remove': [
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
                'django.contrib.messages.middleware.MessageMiddleware',
            ],
        }):
            response = self.client.get('/')
            # ...

Для каждого действия вы можете предоставить либо список значений, либо строку. Если значение уже существует в списке, append и prepend не имеют эффекта; также как и remove, если значение не существует.

override_settings(**kwargs)[исходный код]

Если вы хотите переопределить настройки метода тестирования, Django предоставляет декоратор override_settings() (см. PEP 318). Он используется следующим образом:

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

    @override_settings(LOGIN_URL='/other/login/')
    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

Декоратор также может быть применен к классам TestCase:

from django.test import TestCase, override_settings

@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):

    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')
modify_settings(*args, **kwargs)[исходный код]

Аналогично, Django предоставляет декоратор modify_settings():

from django.test import TestCase, modify_settings

class MiddlewareTestCase(TestCase):

    @modify_settings(MIDDLEWARE={
        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
    })
    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

Декоратор также может быть применен к классам тестовых примеров:

from django.test import TestCase, modify_settings

@modify_settings(MIDDLEWARE={
    'append': 'django.middleware.cache.FetchFromCacheMiddleware',
    'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

Примечание

При передаче класса эти декораторы изменяют класс напрямую и возвращают его; они не создают и не возвращают его модифицированную копию. Поэтому если вы попытаетесь изменить приведенные выше примеры, чтобы присвоить возвращаемому значению имя, отличное от LoginTestCase или MiddlewareTestCase, вы можете с удивлением обнаружить, что исходные классы тестовых примеров по-прежнему одинаково подвержены влиянию декоратора. Для данного класса modify_settings() всегда применяется после override_settings().

Предупреждение

Файл настроек содержит некоторые параметры, к которым обращаются только во время инициализации внутренних механизмов Django. Если вы измените их с помощью override_settings, настройка будет изменена, если вы обратитесь к ней через модуль django.conf.settings, однако, внутренние механизмы Django обращаются к ней по-другому. Эффективно, использование override_settings() или modify_settings() с этими настройками, вероятно, не сделает того, чего вы ожидаете.

Мы не рекомендуем изменять настройку DATABASES. Изменение параметра CACHES возможно, но несколько затруднительно, если вы используете внутренние компоненты, использующие кэширование, например django.contrib.sessions. Например, вам придется заново инициализировать бэкенд сессии в тесте, который использует кэшированные сессии и переопределяет CACHES.

Наконец, избегайте называть свои настройки константами уровня модуля, поскольку override_settings() не будет работать с такими значениями, так как они оцениваются только при первом импорте модуля.

Вы также можете имитировать отсутствие параметра, удалив его после отмены настроек, например, так:

@override_settings()
def test_something(self):
    del settings.LOGIN_URL
    ...

При переопределении настроек убедитесь, что в коде вашего приложения используется кэш или подобная функция, которая сохраняет состояние даже при изменении настроек. Django предоставляет сигнал django.test.signals.setting_changed, который позволяет вам регистрировать обратные вызовы для очистки и другого сброса состояния при изменении настроек.

Сам Django использует этот сигнал для сброса различных данных:

Переопределенные настройки Сброс данных
USE_TZ, TIME_ZONE Часовой пояс баз данных
ТЕМПЛАТЫ Шаблонные двигатели
МОДУЛИ СЕРИАЛИЗАЦИИ Кэш сериализаторов
ЛОКАЛЬНЫЕ_ПУТИ, ЯЗЫКОВОЙ_КОД Перевод по умолчанию и загруженные переводы
MEDIA_ROOT, DEFAULT_FILE_STORAGE Хранилище файлов по умолчанию

Изолирующие приложения¶

utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None

Регистрирует модели, определенные в обернутом контексте, в их собственный изолированный реестр apps. Эта функциональность полезна при создании классов моделей для тестов, так как классы впоследствии будут удалены, и нет риска столкновения имен.

Ярлыки приложений, которые должен содержать изолированный реестр, должны быть переданы в качестве отдельных аргументов. Вы можете использовать isolate_apps() в качестве декоратора или менеджера контекста. Например:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

class MyModelTests(SimpleTestCase):

    @isolate_apps("app_label")
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        ...

… или:

with isolate_apps("app_label"):
    class TestModel(models.Model):
        pass
    ...

Форма декоратора также может быть применена к классам.

Можно указать два необязательных аргумента в виде ключевых слов:

  • attr_name: атрибут, присваиваемый изолированному реестру, если используется в качестве декоратора класса.
  • kwarg_name: аргумент ключевого слова, передающий изолированный реестр, если используется как декоратор функции.

Временный экземпляр Apps, используемый для изоляции регистрации модели, может быть получен как атрибут при использовании в качестве декоратора класса с помощью параметра attr_name:

@isolate_apps("app_label", attr_name="apps")
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)

… или в качестве аргумента метода тестирования при использовании в качестве декоратора метода с помощью параметра kwarg_name:

class TestModelDefinition(SimpleTestCase):
    @isolate_apps("app_label", kwarg_name="apps")
    def test_model_definition(self, apps):
        class TestModel(models.Model):
            pass
        self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)

Опустошение ящика для анализов¶

Если вы используете любой из пользовательских классов Django TestCase, программа запуска тестов будет очищать содержимое тестового почтового ящика в начале каждого тестового случая.

Более подробно об услугах электронной почты во время тестирования см. ниже Email services.

Утверждения¶

Поскольку обычный класс Python unittest.TestCase реализует такие методы утверждения, как assertTrue() и assertEqual(), пользовательский класс Django TestCase предоставляет ряд пользовательских методов утверждения, которые полезны для тестирования веб-приложений:

Сообщения о сбоях, выдаваемые большинством этих методов утверждения, можно настроить с помощью аргумента msg_prefix. Эта строка будет добавлена к любому сообщению о сбое, сгенерированному утверждением. Это позволяет вам предоставить дополнительные подробности, которые могут помочь вам определить место и причину сбоя в вашем тестовом наборе.

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)[исходный код]
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

Утверждает, что выполнение callable вызывает expected_exception и что expected_message находится в сообщении исключения. При любом другом исходе сообщается о неудаче. Это более простая версия unittest.TestCase.assertRaisesRegex() с той разницей, что expected_message не рассматривается как регулярное выражение.

Если заданы только параметры expected_exception и expected_message, возвращает менеджер контекста, так что тестируемый код может быть написан inline, а не как функция:

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)[исходный код]
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

Аналогично SimpleTestCase.assertRaisesMessage(), но для assertWarnsRegex() вместо assertRaisesRegex().

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=»)[исходный код]

Утверждает, что поле формы ведет себя правильно при различных вводах.

Параметры:
  • fieldclass – класс тестируемого поля.
  • valid – словарь, отображающий действительные входные данные на их ожидаемые очищенные значения.
  • invalid – словарь, отображающий недопустимые входные данные на одно или несколько сообщений об ошибках.
  • field_args – args, переданные для инстанцирования поля.
  • field_kwargs – kwargs, переданные для инстанцирования поля.
  • empty_value – ожидаемый чистый выход для входов в empty_values.

Например, следующий код проверяет, что EmailField принимает a@a.com как действительный адрес электронной почты, но отвергает aaa с разумным сообщением об ошибке:

self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase.assertFormError(form, field, errors, msg_prefix=»)[исходный код]

Утверждает, что поле на форме вызывает указанный список ошибок.

form является экземпляром Form. Форма должна быть bound, но не обязательно валидирована (assertFormError() автоматически вызовет full_clean() на форме).

field — это имя поля формы, которое нужно проверить. Чтобы проверить non-field errors, используйте field=None.

errors — это список всех строк ошибок, которые ожидаются в данном поле. Вы также можете передать одну строку ошибок, если вы ожидаете только одну ошибку, что означает, что errors='error message' будет то же самое, что и errors=['error message'].

Changed in Django 4.1:

В старых версиях использование пустого списка ошибок с помощью assertFormError() всегда проходило, независимо от того, были ли ошибки в поле или нет. Начиная с Django 4.1, использование errors=[] будет проходить только в том случае, если поле действительно не имеет ошибок.

Django 4.1 также изменил поведение assertFormError(), когда поле имеет несколько ошибок. В старых версиях, если поле имело несколько ошибок, а вы проверяли только некоторые из них, тест проходил. Начиная с Django 4.1, список ошибок должен точно соответствовать фактическим ошибкам поля.

Не рекомендуется, начиная с версии 4.1: Поддержка передачи объекта ответа и имени формы в assertFormError() устарела и будет удалена в Django 5.0. Вместо этого используйте непосредственно экземпляр формы.

SimpleTestCase.assertFormsetError(formset, form_index, field, errors, msg_prefix=»)[исходный код]

Утверждает, что formset при отображении вызывает указанный список ошибок.

formset является экземпляром Formset. Набор форм должен быть связан, но не обязательно подтвержден (assertFormsetError() будет автоматически вызывать full_clean() на наборе форм).

form_index — это номер формы внутри Formset (начиная с 0). Используйте form_index=None для проверки ошибок, не относящихся к форме, т.е. ошибок, которые вы получаете при вызове formset.non_form_errors(). В этом случае вы также должны использовать field=None.

field и errors имеют то же значение, что и параметры в assertFormError().

Не рекомендуется, начиная с версии 4.1: Поддержка передачи объекта ответа и имени набора форм в assertFormsetError() устарела и будет удалена в Django 5.0. Вместо этого используйте непосредственно экземпляр набора форм.

SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix=», html=False)[исходный код]

Утверждает, что response произвел данный status_code и что text появляется в его content. Если указано count, то text должно встречаться в ответе ровно count раз.

Установите html в True, чтобы обрабатывать text как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на равенстве символов. Пробельные символы в большинстве случаев игнорируются, упорядочивание атрибутов не имеет значения. Более подробную информацию смотрите в assertHTMLEqual().

SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix=», html=False)[исходный код]

Утверждает, что response произвел данный status_code и что text не появляется в его content.

Установите html в True, чтобы обрабатывать text как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на равенстве символов. Пробельные символы в большинстве случаев игнорируются, упорядочивание атрибутов не имеет значения. Более подробную информацию смотрите в assertHTMLEqual().

SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix=», count=None)[исходный код]

Утверждает, что шаблон с заданным именем был использован при визуализации ответа.

response должен быть экземпляром ответа, возвращаемым test client.

template_name должна быть строка, например 'admin/index.html'.

Аргумент count представляет собой целое число, указывающее количество раз, которое шаблон должен быть отображен. По умолчанию None, что означает, что шаблон должен быть отображен один или несколько раз.

Вы можете использовать его в качестве менеджера контекста, например, так:

with self.assertTemplateUsed('index.html'):
    render_to_string('index.html')
with self.assertTemplateUsed(template_name='index.html'):
    render_to_string('index.html')
SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix=»)[исходный код]

Утверждает, что шаблон с заданным именем не использовался при отображении ответа.

Вы можете использовать его в качестве менеджера контекста так же, как и assertTemplateUsed().

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix=»)[исходный код]

Утверждает, что два URL одинаковы, игнорируя порядок параметров строки запроса, за исключением параметров с одинаковым именем. Например, /path/?x=1&y=2 равно /path/?y=2&x=1, но /path/?a=1&a=2 не равно /path/?a=2&a=1.

SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix=», fetch_redirect_response=True)[исходный код]

Утверждает, что response вернул статус перенаправления status_code, перенаправил на expected_url (включая любые данные GET), и что конечная страница была получена с target_status_code.

Если в вашем запросе использовался аргумент follow, то expected_url и target_status_code будут url и код состояния для конечной точки цепочки перенаправления.

Если fetch_redirect_response равно False, конечная страница не будет загружена. Поскольку тестовый клиент не может получать внешние URL, это особенно полезно, если expected_url не является частью вашего приложения Django.

Схема корректно обрабатывается при сравнении двух URL. Если в месте, куда мы перенаправляемся, не указана схема, то используется схема исходного запроса. Если схема присутствует, то для сравнения используется схема в expected_url.

SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)[исходный код]

Утверждает, что строки html1 и html2 равны. Сравнение основано на семантике HTML. При сравнении учитываются следующие моменты:

  • Пробелы до и после HTML-тегов игнорируются.
  • Все типы пробельных символов считаются эквивалентными.
  • Все открытые теги закрываются неявно, например, когда закрывается окружающий тег или заканчивается HTML-документ.
  • Пустые теги эквивалентны их самозакрывающейся версии.
  • Порядок следования атрибутов элемента HTML не имеет значения.
  • Булевы атрибуты (например, checked) без аргумента равны атрибутам, равным по имени и значению (см. примеры).
  • Текст, ссылки на символы и ссылки на сущности, которые ссылаются на один и тот же символ, эквивалентны.

Следующие примеры являются корректными тестами и не вызывают никаких AssertionError:

self.assertHTMLEqual(
    '<p>Hello <b>'world'!</p>',
    '''<p>
        Hello   <b>'world'! </b>
    </p>'''
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>'
)

html1 и html2 должны содержать HTML. Если одно из них не может быть разобрано, будет выдано сообщение AssertionError.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

Changed in Django 4.0:

В старых версиях любой атрибут (не только булевы атрибуты) без значения считался равным атрибуту с тем же именем и значением.

SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)[исходный код]

Утверждает, что строки html1 и html2 не равны. Сравнение основано на семантике HTML. Подробности см. в assertHTMLEqual().

html1 и html2 должны содержать HTML. Если одно из них не может быть разобрано, будет выдано сообщение AssertionError.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)[исходный код]

Утверждает, что строки xml1 и xml2 равны. Сравнение основано на семантике XML. Аналогично assertHTMLEqual(), сравнение производится по разобранному содержимому, поэтому учитываются только семантические различия, а не синтаксические. Если в любом параметре передан недопустимый XML, всегда выдается предупреждение AssertionError, даже если обе строки идентичны.

Объявление XML, тип документа, инструкции по обработке и комментарии игнорируются. Сравниваются только корневой элемент и его дочерние элементы.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)[исходный код]

Утверждает, что строки xml1 и xml2 не равны. Сравнение основано на семантике XML. Подробности см. в assertXMLEqual().

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix=»)[исходный код]

Утверждает, что HTML-фрагмент needle содержится в haystack один раз.

Если указан целочисленный аргумент count, то дополнительно будет строго проверяться количество вхождений needle.

Пробельные символы в большинстве случаев игнорируются, а порядок следования атрибутов не имеет значения. См. раздел assertHTMLEqual() для более подробной информации.

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)[исходный код]

Утверждает, что фрагменты JSON raw и expected_data равны. Применяются обычные правила JSON о несущественных пробельных символах, так как тяжелый груз передается библиотеке json.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)[исходный код]

Утверждает, что фрагменты JSON raw и expected_data не равны. См. assertJSONEqual() для более подробной информации.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

TransactionTestCase.assertQuerysetEqual(qs, values, transform=None, ordered=True, msg=None)[исходный код]

Утверждает, что кверисет qs соответствует определенному итеративу значений values.

Если указано transform, values сравнивается со списком, полученным путем применения transform к каждому члену qs.

По умолчанию сравнение также зависит от порядка. Если qs не обеспечивает неявного упорядочивания, вы можете установить параметр ordered в значение False, что превратит сравнение в сравнение collections.Counter. Если порядок не определен (если данное qs не упорядочено и сравнение производится с более чем одним упорядоченным значением), возникает ошибка ValueError.

Вывод в случае ошибки может быть настроен с помощью аргумента msg.

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)[исходный код]

Утверждает, что когда func вызывается с *args и **kwargs, то выполняются num запросы к базе данных.

Если ключ "using" присутствует в kwargs, он используется в качестве псевдонима базы данных, для которой проверяется количество запросов:

self.assertNumQueries(7, using='non_default_db')

Если вы хотите вызвать функцию с параметром using, вы можете сделать это, обернув вызов символом lambda, чтобы добавить дополнительный параметр:

self.assertNumQueries(7, lambda: my_function(using=7))

Вы также можете использовать его в качестве менеджера контекста:

with self.assertNumQueries(2):
    Person.objects.create(name="Aaron")
    Person.objects.create(name="Daniel")

Тегирование тестов¶

Вы можете пометить свои тесты, чтобы легко запускать определенное подмножество. Например, вы можете пометить быстрые или медленные тесты:

from django.test import tag

class SampleTestCase(TestCase):

    @tag('fast')
    def test_fast(self):
        ...

    @tag('slow')
    def test_slow(self):
        ...

    @tag('slow', 'core')
    def test_slow_but_core(self):
        ...

Вы также можете пометить тестовый пример:

@tag('slow', 'core')
class SampleTestCase(TestCase):
    ...

Подклассы наследуют теги от суперклассов, а методы наследуют теги от своего класса. Дано:

@tag('foo')
class SampleTestCaseChild(SampleTestCase):

    @tag('bar')
    def test(self):
        ...

SampleTestCaseChild.test будет помечен 'slow', 'core', 'bar' и 'foo'.

Затем вы можете выбрать, какие тесты запускать. Например, запустить только быстрые тесты:

/

$ ./manage.py test --tag=fast
...> manage.py test --tag=fast

Или для запуска быстрых тестов и основного (даже если он медленный):

/

$ ./manage.py test --tag=fast --tag=core
...> manage.py test --tag=fast --tag=core

Можно также исключить тесты по тегам. Чтобы запустить основные тесты, если они не медленные:

/

$ ./manage.py test --tag=core --exclude-tag=slow
...> manage.py test --tag=core --exclude-tag=slow

test --exclude-tag имеет приоритет над test --tag, поэтому если в тесте есть два тега и вы выбрали один из них и исключили другой, тест не будет запущен.

Тестирование асинхронного кода¶

Если вы просто хотите протестировать вывод ваших асинхронных представлений, стандартный клиент тестирования запустит их внутри собственного асинхронного цикла без какой-либо дополнительной работы с вашей стороны.

Однако, если вы хотите написать полностью асинхронные тесты для проекта Django, вам нужно будет принять во внимание несколько вещей.

Во-первых, ваши тесты должны быть async def методами на тестовом классе (для того, чтобы дать им асинхронный контекст). Django автоматически обнаружит любые async def тесты и обернет их так, чтобы они выполнялись в собственном цикле событий.

Если вы проводите тестирование из асинхронной функции, вы также должны использовать клиент асинхронного тестирования. Он доступен как django.test.AsyncClient или как self.async_client в любом тесте.

class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, **defaults)[исходный код]

AsyncClient имеет те же методы и сигнатуры, что и синхронный (обычный) тестовый клиент, за двумя исключениями:

  • При инициализации произвольные аргументы ключевых слов в defaults добавляются непосредственно в область видимости ASGI.

  • Параметр follow не поддерживается.

  • Заголовки, передаваемые в качестве аргументов ключевого слова extra, не должны иметь префикса HTTP_, требуемого синхронным клиентом (см. Client.get()). Например, вот как установить заголовок HTTP Accept:

    >>> c = AsyncClient()
    >>> c.get(
    ...     '/customers/details/',
    ...     {'name': 'fred', 'age': 7},
    ...     ACCEPT='application/json'
    ... )
    

При использовании AsyncClient любой метод, выполняющий запрос, должен быть ожидаемым:

async def test_my_thing(self):
    response = await self.async_client.get('/some-url/')
    self.assertEqual(response.status_code, 200)

Асинхронный клиент может также вызывать синхронные представления; он запускается через asynchronous request path Django, который поддерживает оба варианта. Любое представление, вызванное через AsyncClient, получит объект ASGIRequest для своего request, а не WSGIRequest, который создает обычный клиент.

Предупреждение

Если вы используете тестовые декораторы, они должны быть async-совместимыми, чтобы гарантировать их корректную работу. Встроенные в Django декораторы будут вести себя правильно, но сторонние декораторы могут оказаться невыполненными (они «обернут» не ту часть потока выполнения, а не ваш тест).

Если вам необходимо использовать эти декораторы, то вместо них украсьте свои тестовые методы с помощью async_to_sync() внутри них:

from asgiref.sync import async_to_sync
from django.test import TestCase

class MyTests(TestCase):

    @mock.patch(...)
    @async_to_sync
    async def test_my_thing(self):
        ...

Услуги электронной почты¶

Если какое-либо из ваших представлений Django отправляет электронную почту, используя Django’s email functionality, вы, вероятно, не хотите отправлять электронную почту каждый раз, когда запускаете тест, использующий это представление. По этой причине бегунок тестирования Django автоматически перенаправляет все отправленные Django письма в фиктивный почтовый ящик. Это позволяет вам тестировать все аспекты отправки электронной почты — от количества отправленных сообщений до содержимого каждого сообщения — без фактической отправки сообщений.

Тестовый бегунок делает это, прозрачно заменяя обычный почтовый бэкенд на тестовый бэкенд. (Не волнуйтесь — это никак не влияет на другие отправители электронной почты вне Django, например, на почтовый сервер вашей машины, если он у вас есть).

django.core.mail.outbox

Во время тестирования каждое исходящее письмо сохраняется в django.core.mail.outbox. Это список всех экземпляров EmailMessage, которые были отправлены. Атрибут outbox — это специальный атрибут, который создается только при использовании бэкенда электронной почты locmem. Обычно он не существует как часть модуля django.core.mail, и вы не можете импортировать его напрямую. В приведенном ниже коде показано, как правильно обращаться к этому атрибуту.

Вот пример теста, который проверяет django.core.mail.outbox на длину и содержимое:

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            'Subject here', 'Here is the message.',
            'from@example.com', ['to@example.com'],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

Как было отмечено previously, тестовый outbox опустошается в начале каждого теста в Django *TestCase. Чтобы опустошить папку outbox вручную, назначьте пустой список на mail.outbox:

from django.core import mail

# Empty the test outbox
mail.outbox = []

Команды управления¶

Команды управления могут быть проверены с помощью функции call_command(). Вывод может быть перенаправлен в экземпляр StringIO:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

Пропуск тестов¶

Библиотека unittest предоставляет декораторы @skipIf и @skipUnless, позволяющие пропускать тесты, если вы заранее знаете, что эти тесты не пройдут при определенных условиях.

Например, если для успешного выполнения вашего теста требуется определенная дополнительная библиотека, вы можете украсить тестовый пример символом @skipIf. Тогда программа запуска тестов сообщит, что тест не был выполнен и почему, вместо того чтобы выдать ошибку или вообще пропустить тест.

Чтобы дополнить это поведение пропуска тестов, Django предоставляет два дополнительных декоратора пропуска. Вместо проверки общего булева числа эти декораторы проверяют возможности базы данных и пропускают тест, если база данных не поддерживает определенную функцию.

Декораторы используют строковый идентификатор для описания особенностей базы данных. Эта строка соответствует атрибутам класса особенностей подключения к базе данных. Полный список особенностей базы данных, которые могут быть использованы в качестве основы для пропуска тестов, см. в django.db.backends.base.features.BaseDatabaseFeatures class.

skipIfDBFeature(*feature_name_strings)[исходный код]

Пропустите тест на декорирование или TestCase, если все названные функции базы данных поддерживаются.

Например, следующий тест не будет выполнен, если база данных поддерживает транзакции (например, он не будет выполняться в PostgreSQL, но будет выполняться в MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipIfDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings)[исходный код]

Пропустите тест на декорирование или TestCase, если какая-либо из названных функций базы данных не поддерживается.

Например, следующий тест будет выполнен, только если база данных поддерживает транзакции (например, он будет выполняться в PostgreSQL, но не в MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipUnlessDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
  • Назад
  • Обзор: Django
  • Далее

Сайты, в процессе развития и разработки, становится все сложнее тестировать вручную. Кроме такого тестирования, сложными становятся внутренние взаимодействия между компонентами — внесение небольшого изменения в одной части приложения влияет на другие. При этом, чтобы все продолжало работать нужно вносить все больше и больше изменений и, желательно так, чтобы не добавлялись новые ошибки. Одним из способов который позволяет смягчить последствия добавления изменений, является внедрение в разработку автоматического тестирования — оно должно просто и надёжно запускаться каждый раз, когда вы вносите изменения в свой код. Данное руководство рассматривает вопросы автоматизации юнит-тестирования вашего сайта при помощи фреймворка Django для тестов.

Требования: Изучить все предыдущие темы руководства, включая Руководство Django Часть 9: Работа с формами.
Цель: Понимать как создавать юнит тесты для сайта на основе Django.

Обзор

LocalLibrary в настоящий момент содержит страницы для показа списков всех книг, авторов, подробной информации о книгах Book и авторах Author, а также страницу для обновления информации об экземпляре книги BookInstance и, кроме того, страницы для создания, обновления и удаления записей модели Author (и модели Book, в том случае, если вы выполнили домашнее задание в руководстве работа с формами). Даже в случае небольшого сайта, ручной переход на каждую страницу и беглая проверка того, что все работает как следует, может занять несколько минут. В процессе внесения изменений и роста сайта требуемое время для проведения проверок будет только возрастать. Если бы мы продолжили в том же духе, то в какой-то момент на проведение тестов мы тратили бы больше времени, чем на написание кода и внесение изменений.

Автоматические тесты могут серьёзно помочь нам справиться с этой проблемой! Очевидными преимуществами в таком случае являются значительно меньшие временные затраты на проведение тестов, их подробное выполнение, а кроме того, тесты имеют постоянную функциональность, или последовательность действий (человек никогда не сможет тестировать так надёжно!). В связи с быстротой их выполнения автоматические тесты можно выполнять более часто, а если они провалятся, то укажут на соответствующее место (где что-то пошло не так как ожидалось).

Кроме того, автоматические тесты могут действовать как первый «настоящий пользователь» вашего кода, заставляя вас строго следить за объявлениями и документированием поведения вашего сайта. Тесты часто являются основой для создания примеров вашего кода и документации. По этим причинам иногда некоторые процессы разработки программного обеспечения начинаются с определения тестов и их реализации, а уже после этого следует написание кода который должен иметь соответствующее поведение (так называемая разработка на основе тестов и на основе поведения).

Данное руководство показывает процесс создания автоматических тестов в Django при помощи добавления их к разработке сайта LocalLibrary.

Типы тестирования

Существует несколько типов, уровней, классификаций тестов и тестовых приёмов. Наиболее важными автоматическими тестами являются:

Юнит-тесты

Проверяют функциональное поведение для отдельных компонентов, часто классов и функций.

Регрессионное тестирование

Тесты которые воспроизводят исторические ошибки (баги). Каждый тест вначале запускается для проверки того, что баг был исправлен, а затем перезапускается для того, чтобы убедиться, что он не был внесён снова с появлением новых изменений в коде.

Интеграционные тесты

Проверка совместной работы групп компонентов. Данные тесты отвечают за совместную работу между компонентами, не обращая внимания на внутренние процессы в компонентах. Они проводятся как для простых групп компонентов, так и для целых веб-сайтов.

**Примечание:**К другим типам тестов относятся методы чёрного ящика, белого ящика, ручные, автоматические, канареечные (canary), дымные (smoke), соответствия (conformance), принятия (acceptance), функциональные (functional), системные (system), эффективности (performance), загрузочные (load) и стресс-тесты (stress tests).

Что Django предоставляет для тестирования?

Тестирование сайта это сложная задача, потому что она состоит их нескольких логических слоёв – от HTTP-запроса и запроса к моделям, до валидации формы и их обработки, а кроме того, рендеринга шаблонов страниц.

Django предоставляет фреймворк для создания тестов, построенного на основе иерархии классов, которые, в свою очередь, зависят от стандартной библиотеки Python unittest. Несмотря на название, данный фреймворк подходит и для юнит-, и для интеграционного тестирования. Фреймворк Django добавляет методы API и инструменты, которые помогают тестировать как веб так и, специфическое для Django, поведение. Это позволяет вам имитировать URL-запросы, добавление тестовых данных, а также проводить проверку выходных данных ваших приложений. Кроме того, Django предоставляет API (LiveServerTestCase) и инструменты для применения различных фреймворков тестирования, например вы можете подключить популярный фреймворк Selenium (en-US) для имитации поведения пользователя в реальном браузере.

Для написания теста вы должны наследоваться от любого из классов тестирования Django (или юниттеста) (SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase), а затем реализовать отдельные методы проверки кода (тесты это функции-«утверждения», которые проверяют, что результатом выражения являются значения True или False, или что два значения равны и так далее). Когда вы запускаете тест, фреймворк выполняет соответствующие тестовые методы в вашем классе-наследнике. Методы тестирования запускаются независимо друг от друга, начиная с метода настроек и/или завершаясь методом разрушения (tear-down), определённом в классе, как показано ниже.

class YourTestClass(TestCase):

    def setUp(self):
        # Установки запускаются перед каждым тестом
        pass

    def tearDown(self):
        # Очистка после каждого метода
        pass

    def test_something_that_will_pass(self):
        self.assertFalse(False)

    def test_something_that_will_fail(self):
        self.assertTrue(False)

Самый подходящий базовый класс для большинства тестов это django.test.TestCase. Этот класс создаёт чистую базу данных перед запуском своих методов, а также запускает каждую функцию тестирования в его собственной транзакции. У данного класса также имеется тестовый Клиент, который вы можете использовать для имитации взаимодействия пользователя с кодом на уровне отображения. В следующих разделах мы сконцентрируемся на юнит-тестах, которые будут созданы на основе класса TestCase.

Примечание: Класс django.test.TestCase очень удобен, но он может приводить к замедленной работе в некоторых случаях (не для каждого теста необходимо настраивать базу данных, или имитировать взаимодействие с отображением). Когда вы познакомитесь с работой данного класса, то сможете заменить некоторые из ваших тестов на более простые классы тестирования.

Что вы должны тестировать?

Вы должны тестировать все аспекты, касающиеся вашего кода, но не библиотеки, или функциональность, предоставляемые Python, или Django.

Например, рассмотрим модель Author, определённую ниже. Вам не нужно проверять тот факт, что first_name и last_name были сохранены в базу данных как CharField, потому что за это отвечает непосредственно Django (хотя конечно, на практике в течение разработки вы косвенно будете проверять данную функциональность). Тоже касается и, например, проверки того, что поле date_of_birth является датой, поскольку это тоже часть реализации Django.

Вы должны проверить текст для меток (First name, Last_name, Date of birth, Died), и размер поля, выделенного для текста (100 символов), потому что они являются частью вашей разработки и чем-то, что может сломаться/измениться в будущем.

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

Подобным же образом вы должны убедиться, что методы get_absolute_url() и __str__() ведут себя как требуется, потому что они являются частью вашей бизнес логики. В случае функции get_absolute_url() вы можете быть уверены, что функция из Django reverse() была реализована правильно и, следовательно, вы тестируете только то, чтобы соответствующий вызов в отображении был правильно определён.

Примечание: Проницательные читатели могут заметить, что мы можем некоторым образом ограничить дату рождения и смерти какими-то граничными значениями и выполнять проверку, чтобы дата смерти шла после рождения. В Django данное ограничение может быть добавлено к вашим классам форм (хотя вы и можете определить валидаторы для этих полей, они будут проявлять себя только на уровне форм, а не уровне модели).

Ну что же, усвоив данную информацию, давайте перейдём к процессу определения и запуска тестов.

Обзор структуры тестов

Перед тем как мы перейдём к тому «что тестировать», давайте кратко взглянем на моменты где и как определяются тесты.

Django использует юнит-тестовый модуль — встроенный «обнаружитель» тестов, который находит тесты в текущей рабочей директории, в любом файле с шаблонным именем test*.py. Предоставляя соответствующие имена файлов, вы можете работать с любой структурой которая вас устраивает. Мы рекомендуем создать пакет для вашего тестирующего кода и, следовательно, отделить файлы моделей, отображений, форм и любые другие, от кода который будет использоваться для тестов. Например:

catalog/
  /tests/
    __init__.py
    test_models.py
    test_forms.py
    test_views.py

В проекте LocalLibrary создайте файловую структуру, указанную выше. Файл __init__.py должен быть пустым (так мы говорим Питону, что данная директория является пакетом). Вы можете создать три тестовых файла при помощи копирования и переименования файла-образца /catalog/tests.py.

Примечание: Скелет тестового файла /catalog/tests.py был создан автоматически когда мы выполняли построение скелета сайта Django. Является абсолютно «легальным» действием — поместить все ваши тесты в данный файл, тем не менее, если вы проводите тесты «правильно», то вы очень быстро придёте к очень большому и неуправляемому файлу тестирования.

Можете удалить данный файл, поскольку больше он нам не понадобится.

Откройте /catalog/tests/test_models.py. Файл должен импортировать django.test.TestCase, как показано ниже:

from django.test import TestCase

# Поместите ваш код тестов здесь

Вы часто будете добавлять соответствующий тестовый класс для каждой модели/отображения/формы с отдельными методами проверки каждой отдельной функциональности. В каких-то случаях вы захотите иметь отдельный класс для тестирования какого-то особого варианта работы, или функциональности, с отдельными функциями тестирования, которые будут проверять элемент/элементы данного варианта (например, мы можем создать отдельный класс тестирования для проверки того, что поле валидно, — функции данного класса будут проверять каждый неверный вариант использования). Опять же, структура файлов и пакетов полностью зависит от вас и будет лучше если вы будете её придерживаться.

Добавьте тестовый класс, показанный ниже, в нижнюю часть файла. Данный класс демонстрирует как создать класс тестирования при помощи наследования от TestCase.

class YourTestClass(TestCase):

    @classmethod
    def setUpTestData(cls):
        print("setUpTestData: Run once to set up non-modified data for all class methods.")
        pass

    def setUp(self):
        print("setUp: Run once for every test method to setup clean data.")
        pass

    def test_false_is_false(self):
        print("Method: test_false_is_false.")
        self.assertFalse(False)

    def test_false_is_true(self):
        print("Method: test_false_is_true.")
        self.assertTrue(False)

    def test_one_plus_one_equals_two(self):
        print("Method: test_one_plus_one_equals_two.")
        self.assertEqual(1 + 1, 2)

Этот класс определяет два метода которые вы можете использовать для дотестовой настройки (например, создание какой-либо модели, или других объектов, которые вам понадобятся):

  • setUpTestData() вызывается каждый раз перед запуском теста на уровне настройки всего класса. Вы должны использовать данный метод для создания объектов, которые не будут модифицироваться/изменяться в каком-либо из тестовых методов.
  • setUp() вызывается перед каждой тестовой функцией для настройки объектов, которые могут изменяться во время тестов (каждая функция тестирования будет получать «свежую» версию данных объектов).

Примечание: . Классы тестирования также содержат метод tearDown(), который мы пока не используем. Этот метод не особенно полезен для тестирования баз данных, поскольку базовый класс TestCase автоматически разрывает соединения с ними.

Далее идут несколько методов, которые используют функции Assert, проверяющие условия «истинно» (true), «ложно» (false) или равенство (AssertTrue, AssertFalse, AssertEqual). Если условия не выполняются как ожидалось, то это приводит к провалу теста и выводу соответствующего сообщения об ошибке на консоль.

Функции проверки утверждений AssertTrue, AssertFalse, AssertEqual реализованы в unittest. В данном фреймворке существуют и другие подобные функции, а кроме того, специфические для Django функции проверки, например, перехода из/к отображению (assertRedirects), проверки использования какого-то конкретного шаблона (assertTemplateUsed) и так далее.

Примечание: В обычной ситуации у вас нет необходимости вызывать функции print() из методов теста, как во фрагменте выше. Мы поступили так только для того, чтобы вы в консоле увидели порядок вызова тестовых функций класса.

Как запускать тесты

Простейшим способом запуска всех тестов является применение следующей команды:

Таким образом мы найдём в текущей директории все файлы с именем test*.py и запустим все тесты (у нас имеются несколько файлов для тестирования, но на данный момент, только /catalog/tests/test_models.py содержит какие-либо тесты). По умолчанию, тесты сообщат что-нибудь, только в случае провала.

Запустите тесты из корневой папки сайта LocalLibrary. Вы должны увидеть вывод, который похож на следующий.

>python manage.py test

Creating test database for alias 'default'...
setUpTestData: Run once to set up non-modified data for all class methods.
setUp: Run once for every test method to setup clean data.
Method: test_false_is_false.
.setUp: Run once for every test method to setup clean data.
Method: test_false_is_true.
.setUp: Run once for every test method to setup clean data.
Method: test_one_plus_one_equals_two.
.
======================================================================
FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:Githubdjango_tmplibrary_w_t_2locallibrarycatalogteststests_models.py", line 22, in test_false_is_true
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 3 tests in 0.075s

FAILED (failures=1)
Destroying test database for alias 'default'...

Как видите, один тест провалился и мы можем точно увидеть в какой именно функции это произошло и почему (так и было задумано, поскольку False не равен True!).

Примечание: Совет: Самая важная вещь, которую нужно извлечь из тестового выхода выше, заключается в том, что это гораздо более ценно, если вы используете описательные/информативные имена для ваших объектов и методов.

Текст выделенный жирным, обычно не должен появляться в тестовом выводе (это результат работы функций print() в наших тестах). Он показывает, что вызов метода setUpTestData() происходит один раз для всего класса в целом, а вызовыsetUp() осуществляются перед каждым методом.

Следующий раздел показывает как запускать отдельные тесты и как контролировать процесс вывода информации.

Ещё больше тестовой информации

Если вы желаете получать больше информации о тестах вы должны изменить значение параметра verbosity. Например, для вывода списка успешных и неуспешных тестов (и всю информацию о том, как прошла настройка базы данных) вы можете установить значение verbosity равным «2»:

python3 manage.py test --verbosity 2

Доступными значениями для verbosity являются 0, 1 (значение по умолчанию), 2 и 3.

Запуск определённых тестов

Если вы хотите запустить подмножество тестов, тогда вам надо указать полный путь к вашему пакету, модулю/подмодулю, классу наследнику TestCase, или методу:

python3 manage.py test catalog.tests   # Run the specified module
python3 manage.py test catalog.tests.test_models  # Run the specified module
python3 manage.py test catalog.tests.test_models.YourTestClass # Run the specified class
python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two  # Run the specified method

Тестирование LocalLibrary

Теперь, когда мы знаем как запустить наши тесты и что именно мы должны тестировать, давайте рассмотрим некоторые практические примеры.

**Примечание:**Мы не будем расписывать все тесты, а просто покажем вам пример того, как они должны работать и что ещё вы можете с ними сделать.

Модели

Как было отмечено ранее, мы должны тестировать все то, что является частью нашего кода, а не библиотеки/код, которые уже были протестированы командами разработчиков Django, или Python.

Рассмотрим модель Author. Мы должны провести тесты текстовых меток всех полей, поскольку, даже несмотря на то, что не все они определены, у нас есть проект, в котором сказано, что все их значения должны быть заданы. Если мы не проведём их тестирование, тогда мы не будем знать, что данные метки действительно содержат необходимые значения. Мы уверены в том, что Django создаст поле заданной длины, таким образом наши тесты будут проверять нужный нам размер поля, а заодно и его содержимое.

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return '%s, %s' % (self.last_name, self.first_name)

Откройте файл /catalog/tests/test_models.py и замените все его содержимое кодом, приведённом во фрагменте для тестирования модели Author (фрагмент представлен ниже).

В первой строке мы импортируем класс TestCase, а затем наследуемся от него, создавая класс с описательным именем (AuthorModelTest), оно поможет нам идентифицировать места провалов в тестах во время вывода информации на консоль. Затем мы создаём метод setUpTestData(), в котором создаём объект автора, который мы будем использовать в тестах, но нигде не будем изменять.

from django.test import TestCase

# Create your tests here.

from catalog.models import Author

class AuthorModelTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        #Set up non-modified objects used by all test methods
        Author.objects.create(first_name='Big', last_name='Bob')

    def test_first_name_label(self):
        author=Author.objects.get(id=1)
        field_label = author._meta.get_field('first_name').verbose_name
        self.assertEquals(field_label,'first name')

    def test_date_of_death_label(self):
        author=Author.objects.get(id=1)
        field_label = author._meta.get_field('date_of_death').verbose_name
        self.assertEquals(field_label,'died')

    def test_first_name_max_length(self):
        author=Author.objects.get(id=1)
        max_length = author._meta.get_field('first_name').max_length
        self.assertEquals(max_length,100)

    def test_object_name_is_last_name_comma_first_name(self):
        author=Author.objects.get(id=1)
        expected_object_name = '%s, %s' % (author.last_name, author.first_name)
        self.assertEquals(expected_object_name,str(author))

    def test_get_absolute_url(self):
        author=Author.objects.get(id=1)
        #This will also fail if the urlconf is not defined.
        self.assertEquals(author.get_absolute_url(),'/catalog/author/1')

Тесты полей проверяют значения текстовых меток (verbose_name), включая их ожидаемую длину. Все методы имеют описательные имена, а их логика придерживается одной и той же структуры:

# Получение объекта для тестирования
author=Author.objects.get(id=1)

# Получение метаданных поля для получения необходимых значений
field_label = author._meta.get_field('first_name').verbose_name

# Сравнить значение с ожидаемым результатом
self.assertEquals(field_label,'first name')

Интересно отметить следующее:

  • Мы не можем получить поле verbose_name напрямую через author.first_name.verbose_name, потому что author.first_name является строкой. Вместо этого, нам надо использовать атрибут _meta объекта автора для получения того экземпляра поля, который будет использоваться для получения дополнительной информации.
  • Мы выбрали метод assertEquals(field_label,'first name') вместо assertTrue(field_label == 'first name'), потому что, в случае провала теста, в выводе будет указано какое именно значение содержит метка и это немного облегчит нам задачу по отладке кода.

Примечание: Тесты для текстовых меток last_name и date_of_birth, а также тест длины поля last_name были опущены. Добавьте свою версию этих тестов, соблюдая соглашение об именовании и следуя структуре логики, представленной выше.

Кроме того, нам надо провести тесты наших собственных методов. Они просто проверяют, что имена объектов имеют следующие значения «Last Name, First Name» и что URL-адрес, по которому мы получаем экземпляр Author, такой как ожидается.

def test_object_name_is_last_name_comma_first_name(self):
    author=Author.objects.get(id=1)
    expected_object_name = '%s, %s' % (author.last_name, author.first_name)
    self.assertEquals(expected_object_name,str(author))

def test_get_absolute_url(self):
    author=Author.objects.get(id=1)
    #This will also fail if the urlconf is not defined.
    self.assertEquals(author.get_absolute_url(),'/catalog/author/1')

Теперь запустите тесты. Если вы создали модель Author, в соответствии с разделом о моделях данного руководства, то весьма вероятно, что вы получите сообщение об ошибке для метки date_of_death, как показано ниже. Тест провалился потому что, в соответствии с соглашением Django, первый символ имени метки должен быть в верхнем регистре (Django делает это автоматически).

======================================================================
FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:...locallibrarycatalogteststest_models.py", line 32, in test_date_of_death_label
    self.assertEquals(field_label,'died')
AssertionError: 'Died' != 'died'
- Died
? ^
+ died
? ^

Это несущественный баг, но он демонстрирует нам то, что написание тестов может более тщательно проверить все неточности, которые вы можете сделать.

**Примечание:**Измените значение метки для поля date_of_death (/catalog/models.py) на «died» и перезапустите тесты.

Тот же подход применяется к тестированию других моделей. Самостоятельно создайте свои собственные тесты для оставшихся моделей.

Формы

Смысл проведения тестов для форм тот же, что и для моделей; надо проверить весь собственный код и другие особенности проекта, но не то, что касается фреймворка, или сторонних библиотек.

В основном это означает, что вы должны протестировать то, что формы имеют соответствующие поля и что они показываются с соответствующими метками и вспомогательными текстами. Вам не надо проверять то, что Django правильно осуществляет валидацию полей (если только вы не создали своё собственное поле и валидацию) — то есть вам не надо проверять что, например, поле ввода электронного адреса принимает только электронного адреса. Но вы должны протестировать каждую дополнительную валидацию, которую вы добавляете для полей и любые сообщения, который ваш код генерирует в случае ошибок.

Рассмотрим форму для обновления книг. Она имеет только одно поле обновления даты, которое будет иметь текстовую метку и вспомогательный текст, который вам надо проверить.

class RenewBookForm(forms.Form):
    """
    Форма обновления книг для библиотекарей
    """
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean_renewal_date(self):
        data = self.cleaned_data['renewal_date']

        #Проверка, что дата не в прошлом.
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))
        #Если дата в "далёком" будущем (+4 недели)
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Всегда надо возвращать очищенные данные.
        return data

Откройте файл /catalog/tests/test_forms.py и замените весь существующий в нем код, следующим кодом теста для формы RenewBookForm. Мы начали его с импорта нашей формы и некоторых библиотек Python и Django, которые помогут нам провести тесты. Затем, тем же способом как мы делали для моделей, объявляем тестовый класс нашей формы, то есть применяя описательное имя класс наследника TestCase.

from django.test import TestCase

# Создайте ваши тесты здесь

import datetime
from django.utils import timezone
from catalog.forms import RenewBookForm

class RenewBookFormTest(TestCase):

    def test_renew_form_date_field_label(self):
        form = RenewBookForm()
        self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')

    def test_renew_form_date_field_help_text(self):
        form = RenewBookForm()
        self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).')

    def test_renew_form_date_in_past(self):
        date = datetime.date.today() - datetime.timedelta(days=1)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertFalse(form.is_valid())

    def test_renew_form_date_too_far_in_future(self):
        date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertFalse(form.is_valid())

    def test_renew_form_date_today(self):
        date = datetime.date.today()
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertTrue(form.is_valid())

    def test_renew_form_date_max(self):
        date = timezone.now() + datetime.timedelta(weeks=4)
        form_data = {'renewal_date': date}
        form = RenewBookForm(data=form_data)
        self.assertTrue(form.is_valid())

Первые две функции проверяют текст который должны содержать поля label и help_text. Доступ к полю мы получаем при помощи словаря (то есть, form.fields['renewal_date']). Отметим, что мы должны проверять содержит ли метка значение None, иначе в поле текста метки вы увидите «None«.

Оставшиеся функции проверяют валидность дат, то есть их нахождение внутри определённого интервала, а также невалидность для значений, которые находятся вне заданного интервала. Для получения исходного значения мы использовали функцию получения текущей даты (datetime.date.today()), а также функцию datetime.timedelta() (которая принимает определённое число дней, или недель). Затем мы просто создали форму, передавая ей наши данные и проверяя её на валидность.

Примечание: В данном примере мы не использовали ни базу данных, ни тестовый клиент. Рассмотрите модификацию этих тестов при помощи класса SimpleTestCase.

Нам также надо бы проверять возникновение ошибок, которые появляются если форма не валидна. Но, обычно, это относится к процессу вывода информации, таким образом, мы позаботимся об этом в следующем разделе.

На этом с формами можно закончить; у нас имеются и другие тесты, но они были созданы обобщёнными классами отображения для редактирования! Запустите тесты и убедитесь, что наш код все ещё им соответствует!

Отображения

Для проверки поведения отображения мы используем тестовый клиент Django Client. Данный класс действует как упрощённый веб-браузер который мы применяем для имитации GET и POST запросов и проверки ответов. Про ответы мы можем узнать почти все, начиная с низкоуровневого HTTP (итоговые заголовки и коды статусов) и вплоть до применяемых шаблонов, которые используются для HTML-рендера, а также контекста, который передаётся в соответствующий шаблон. Кроме того, мы можем отследить последовательность перенаправлений (если имеются), проверить URL-адреса и коды статусов на каждом шаге. Все это позволит нам проверить, что каждое отображение выполняет то, что ожидается.

Давайте начнём с одного из простейших отображений которое возвращает список всех авторов. Вы можете его увидеть по URL-адресу /catalog/authors/ (данный URL-адрес можно найти в разделе приложения catalog, в файле настроек urls.py по имени ‘authors’).

class AuthorListView(generic.ListView):
    model = Author
    paginate_by = 10

Поскольку это обобщённое отображение списка, то почти все за нас делает Django. Если вы доверяете Django, то единственной вещью, которую вам нужно протестировать, является переход к данному отображению по указанному URL-адресу. Таким образом, если вы применяете методику TDD (test-driven development, разработка через тесты), то начните проект с написания тестов, которые будут проверять, что данное отображение выводит всех авторов и, к тому же, например, блоками по 10.

Откройте файл /catalog/tests/test_views.py замените все его содержимое на следующий код теста для класса AuthorListView. Как и ранее, мы импортируем нашу модель и некоторые полезные классы. В методе setUpTestData() мы задаём число объектов класса Author которые мы тестируем при постраничном выводе.

from django.test import TestCase

# Create your tests here.

from catalog.models import Author
from django.urls import reverse

class AuthorListViewTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        #Create 13 authors for pagination tests
        number_of_authors = 13
        for author_num in range(number_of_authors):
            Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,)

    def test_view_url_exists_at_desired_location(self):
        resp = self.client.get('/catalog/authors/')
        self.assertEqual(resp.status_code, 200)

    def test_view_url_accessible_by_name(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)

    def test_view_uses_correct_template(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)

        self.assertTemplateUsed(resp, 'catalog/author_list.html')

    def test_pagination_is_ten(self):
        resp = self.client.get(reverse('authors'))
        self.assertEqual(resp.status_code, 200)
        self.assertTrue('is_paginated' in resp.context)
        self.assertTrue(resp.context['is_paginated'] == True)
        self.assertTrue( len(resp.context['author_list']) == 10)

    def test_lists_all_authors(self):
        #Get second page and confirm it has (exactly) remaining 3 items
        resp = self.client.get(reverse('authors')+'?page=2')
        self.assertEqual(resp.status_code, 200)
        self.assertTrue('is_paginated' in resp.context)
        self.assertTrue(resp.context['is_paginated'] == True)
        self.assertTrue( len(resp.context['author_list']) == 3)

Все тесты используют клиент (принадлежащего классу TestCase, от которого мы наследовались) для имитации GET-запроса и получения ответа (resp). Первая версия проверяет заданный URL-адрес (заметьте, — просто определённый путь без указания домена), в то время как второй генерирует URL-адрес при помощи его имени, указанного в настройках.

resp = self.client.get('/catalog/authors/')
resp = self.client.get(reverse('authors'))

Когда мы получаем ответ, то мы извлекаем код статуса, используемый шаблон, «включён» ли постраничный вывод, количество элементов в подмножестве (на странице) и общее число элементов.

Наиболее интересной переменной является resp.context, которая является объектом контекста, который передаётся шаблону из отображения. Он (объект контекста) очень полезен для тестов, поскольку позволяет нам убедиться, что наш шаблон получает все данные которые ему необходимы. Другими словами мы можем проверить, что мы используем правильный шаблон с данными, которые проделывают долгий путь проверок чтобы соответствовать данному шаблону.

Отображения и регистрация пользователей

В некоторых случаях вам нужно провести тесты отображений к которым имеют доступ только зарегистрированные пользователи. Например, LoanedBooksByUserListView очень похоже на наше предыдущее отображение, но доступно только для залогинившихся пользователей и показывает только те записи (BookInstance), которые соответствуют текущему пользователю, имеют статус ‘on loan’ (книга взята домой), а также забронированы.

from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
    """
    Обобщённый класс отображения списка взятых книг текущим пользователем
    """
    model = BookInstance
    template_name ='catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')

Добавьте тестовый код следующего фрагмента в /catalog/tests/test_views.py. В нем, для создания нескольких аккаунтов и объектов BookInstance которые будут использоваться в дальнейших тестах, мы используем метод SetUp() (вместе с соответствующими книгами и другими записями). Половина книг бронируется тестовыми пользователями, но в начале для них всех мы устанавливаем статус «доступно». Использование метода SetUp() предпочтительнее чем setUpTestData(), поскольку в дальнейшем мы будем модифицировать некоторые объекты.

Примечание: Метод setUp() создаёт книгу с заданным языком Language, но ваш код может не включать в себя модель Language, поскольку это было домашним заданием. В таком случае просто закомментируйте соответствующие строки. Поступите также и в следующем разделе, посвящённом RenewBookInstancesViewTest.

import datetime
from django.utils import timezone

from catalog.models import BookInstance, Book, Genre, Language
from django.contrib.auth.models import User # Необходимо для представления User как borrower

class LoanedBookInstancesByUserListViewTest(TestCase):

    def setUp(self):
        # Создание двух пользователей
        test_user1 = User.objects.create_user(username='testuser1', password='12345')
        test_user1.save()
        test_user2 = User.objects.create_user(username='testuser2', password='12345')
        test_user2.save()

        # Создание книги
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language)
        # Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre.set(genre_objects_for_book) # Присвоение типов many-to-many напрямую недопустимо
        test_book.save()

        # Создание 30 объектов BookInstance
        number_of_book_copies = 30
        for book_copy in range(number_of_book_copies):
            return_date= timezone.now() + datetime.timedelta(days=book_copy%5)
            if book_copy % 2:
                the_borrower=test_user1
            else:
                the_borrower=test_user2
            status='m'
            BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status)

    def test_redirect_if_not_logged_in(self):
        resp = self.client.get(reverse('my-borrowed'))
        self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/')

    def test_logged_in_uses_correct_template(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))

        # Проверка что пользователь залогинился
        self.assertEqual(str(resp.context['user']), 'testuser1')
        # Проверка ответа на запрос
        self.assertEqual(resp.status_code, 200)

        # Проверка того, что мы используем правильный шаблон
        self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')

Если пользователь не залогирован то, чтобы убедиться в том что отображение перейдёт на страницу входа (логирования), мы используем метод assertRedirects, что продемонстрировано в методе test_redirect_if_not_logged_in(). Затем мы осуществляем вход для пользователя и проверяем что полученный статус status_code равен 200 (успешно).

Остальные тесты проверяют, соответственно, что наше отображение показывает только те книги которые взяты текущим пользователем. Скопируйте код, показанный ниже, в нижнюю часть предыдущего класса.

    def test_only_borrowed_books_in_list(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))

        #Проверка, что пользователь залогинился
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)

        #Проверка, что изначально у нас нет книг в списке
        self.assertTrue('bookinstance_list' in resp.context)
        self.assertEqual( len(resp.context['bookinstance_list']),0)

        #Теперь все книги "взяты на прокат"
        get_ten_books = BookInstance.objects.all()[:10]

        for copy in get_ten_books:
            copy.status='o'
            copy.save()

        #Проверка, что все забронированные книги в списке
        resp = self.client.get(reverse('my-borrowed'))
        #Проверка, что пользователь залогинился
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Проверка успешности ответа
        self.assertEqual(resp.status_code, 200)

        self.assertTrue('bookinstance_list' in resp.context)

        #Подтверждение, что все книги принадлежат testuser1 и взяты "на прокат"
        for bookitem in resp.context['bookinstance_list']:
            self.assertEqual(resp.context['user'], bookitem.borrower)
            self.assertEqual('o', bookitem.status)

    def test_pages_ordered_by_due_date(self):

        #Изменение статуса на "в прокате"
        for copy in BookInstance.objects.all():
            copy.status='o'
            copy.save()

        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('my-borrowed'))

        #Пользователь залогинился
        self.assertEqual(str(resp.context['user']), 'testuser1')
        #Check that we got a response "success"
        self.assertEqual(resp.status_code, 200)

        #Подтверждение, что из всего списка показывается только 10 экземпляров
        self.assertEqual( len(resp.context['bookinstance_list']),10)

        last_date=0
        for copy in resp.context['bookinstance_list']:
            if last_date==0:
                last_date=copy.due_back
            else:
                self.assertTrue(last_date <= copy.due_back)

Если хотите, то вы, безусловно, можете добавить тесты проверяющие постраничный вывод!

Тестирование форм и отображений

Процесс тестирования отображений с формами немного более сложен, чем в представленных ранее случаях, поскольку вам надо протестировать большее количество кода: начальное состояние показа формы, показ формы и её данных в случае ошибок, а также показ формы в случае успеха. Хорошей новостью является то, что мы применяем клиент для тестирования практически тем же способом, как мы делали это в случае отображений, которые отвечают только за вывод информации.

В качестве демонстрации давайте напишем некоторые тесты для отображения, которые отвечают за обновление книг(renew_book_librarian()):

from .forms import RenewBookForm

@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
    """
    Функция отображения обновления экземпляра BookInstance библиотекарем
    """
    book_inst=get_object_or_404(BookInstance, pk = pk)

    # Если это POST-запрос, тогда обработать данные формы
    if request.method == 'POST':

        # Создать объект формы и заполнить её данными из запроса (связывание/биндинг):
        form = RenewBookForm(request.POST)

        # Проверка валидности формы:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_inst.due_back = form.cleaned_data['renewal_date']
            book_inst.save()

            # переход по URL-адресу:
            return HttpResponseRedirect(reverse('all-borrowed') )

    # Если это GET-запрос (или что-то ещё), то создаём форму по умолчанию
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,})

    return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})

Нам надо проверить что к данному отображению имеют доступ только те пользователи, которые имеют разрешение типа can_mark_returned, а кроме того, что пользователи перенаправляются на страницу ошибки HTTP 404 если они пытаются обновить экземпляр книги BookInstance, который не существует. Мы должны проверить что начальное значение формы соответствует дате через 3 недели в будущем, а также то, что если форма прошла валидацию, то мы переходим на страницу отображения книг «all-borrowed» (забронированных). Для тестов, отвечающих за проверку «провалов», мы также должны удостовериться что они отправляют соответствующие сообщения об ошибках.

В нижнюю часть файла /catalog/tests/test_views.py добавьте класс тестирования (показан во фрагменте, ниже). Он создаёт двух пользователей и два экземпляра книги, но только один пользователь получает необходимый доступ к соответствующему отображению. Код, который «присваивает» соответствующий доступ, выделен в коде жирным:

from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.

class RenewBookInstancesViewTest(TestCase):

    def setUp(self):
        #Создание пользователя
        test_user1 = User.objects.create_user(username='testuser1', password='12345')
        test_user1.save()

        test_user2 = User.objects.create_user(username='testuser2', password='12345')
        test_user2.save()
        permission = Permission.objects.get(name='Set book as returned')
        test_user2.user_permissions.add(permission)
        test_user2.save()

        #Создание книги
        test_author = Author.objects.create(first_name='John', last_name='Smith')
        test_genre = Genre.objects.create(name='Fantasy')
        test_language = Language.objects.create(name='English')
        test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
        #Создание жанра Create genre as a post-step
        genre_objects_for_book = Genre.objects.all()
        test_book.genre=genre_objects_for_book
        test_book.save()

        #Создание объекта BookInstance для для пользователя test_user1
        return_date= datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o')

        #Создание объекта BookInstance для для пользователя test_user2
        return_date= datetime.date.today() + datetime.timedelta(days=5)
        self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')

В нижнюю часть класса тестирования добавьте следующие методы (из следующего фрагмента). Они проверяют, что только пользователь с соответствующим доступом (testuser2) имеет доступ к отображению. Мы проверяем все случаи: когда пользователь не залогинился, когда залогинился, но не имеет соответствующего доступа, когда имеет доступ, но не является заёмщиком книги (тест должен быть успешным), а также, что произойдёт если попытаться получить доступ к книге BookInstance которой не существует. Кроме того, мы проверяем то, что используется правильный (необходимый) шаблон.

    def test_redirect_if_not_logged_in(self):
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
        self.assertEqual( resp.status_code,302)
        self.assertTrue( resp.url.startswith('/accounts/login/') )

    def test_redirect_if_logged_in_but_not_correct_permission(self):
        login = self.client.login(username='testuser1', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )

        #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
        self.assertEqual( resp.status_code,302)
        self.assertTrue( resp.url.startswith('/accounts/login/') )

    def test_logged_in_with_permission_borrowed_book(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) )

        #Check that it lets us login - this is our book and we have the right permissions.
        self.assertEqual( resp.status_code,200)

    def test_logged_in_with_permission_another_users_borrowed_book(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )

        #Check that it lets us login. We're a librarian, so we can view any users book
        self.assertEqual( resp.status_code,200)

    def test_HTTP404_for_invalid_book_if_logged_in(self):
        import uuid
        test_uid = uuid.uuid4() #unlikely UID to match our bookinstance!
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) )
        self.assertEqual( resp.status_code,404)

    def test_uses_correct_template(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        self.assertEqual( resp.status_code,200)

        #Check we used correct template
        self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')

Добавьте ещё один тестовый метод, показанный ниже. Он проверяет что начальная дата равна трём неделям в будущем. Заметьте, что мы имеем возможность получить доступ к начальному значению из поля формы (выделено жирным).

    def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
        login = self.client.login(username='testuser2', password='12345')
        resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
        self.assertEqual( resp.status_code,200)

        date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
        self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )

Следующий тест (тоже добавьте его в свой класс) проверяет что отображение, в случае успеха, перенаправляет пользователя к списку всех забронированных книг. Здесь мы показываем как при помощи клиента вы можете создать и передать данные в POST-запросе. Данный запрос передаётся вторым аргументом в пост-функцию и представляет из себя словарь пар ключ/значение.

    def test_redirects_to_all_borrowed_book_list_on_success(self):
        login = self.client.login(username='testuser2', password='12345')
        valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} )
        self.assertRedirects(resp, reverse('all-borrowed') )

Предупреждение: Вместо перехода к отображению all-borrowed, добавленного в качестве домашнего задания, вы можете перенаправить пользователя на домашнюю страницу ‘/’. В таком случае, исправьте две последние строки тестового кода на код, показанный ниже. Присваивание follow=True, в запросе, гарантирует что запрос вернёт окончательный URL-адрес пункта назначения (следовательно проверяется /catalog/, а не /).

 resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future},follow=True )
 self.assertRedirects(resp, '/catalog/')

Скопируйте две последние функции в класс, представленные ниже. Они тоже проверяют POST-запросы, но для случая неверных дат. Мы используем функцию assertFormError(), чтобы проверить сообщения об ошибках.

    def test_form_invalid_renewal_date_past(self):
        login = self.client.login(username='testuser2', password='12345')
        date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} )
        self.assertEqual( resp.status_code,200)
        self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past')

    def test_form_invalid_renewal_date_future(self):
        login = self.client.login(username='testuser2', password='12345')
        invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
        resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} )
        self.assertEqual( resp.status_code,200)
        self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')

Такие же способы тестирования могут применяться для проверок других отображений.

Шаблоны

Django предоставляет API для тестирования, которое проверяет что функции отображения вызывают правильные шаблоны, а также позволяют убедиться, что им передаётся соответствующая информация. Кроме того, в Django имеется возможность использовать сторонние API для проверок того, что ваш HTML показывает то, что надо.

Другие рекомендованные инструменты для тестирования

Django фреймворк для тестирования помогает вам создавать эффективные юнит- и интеграционные тесты — мы рассмотрели только небольшую часть того, что может делать фреймворк unittest и совсем не упоминали дополнения Django (например, посмотрите на модуль unittest.mock, который подключает сторонние библиотеки тестирования).

Из всего множества сторонних инструментов тестирования, мы кратко опишем возможности двух:

  • Coverage: Это инструмент Python, который формирует отчёты о том, какое количество кода выполняется во время проведения тестов. Это полезно для уточнения степени «покрытия» кода тестами.
  • Selenium (en-US) это фреймворк проведения автоматического тестирования в настоящем браузере. Он позволяет вам имитировать взаимодействие пользователя с вашим сайтом (что является следующим шагом в проведении интеграционных тестов).

Домашняя работы

Существуют другие модели и отображения, которые мы могли бы протестировать. В качестве простого упражнения, попробуйте создать тестовый вариант для отображения AuthorCreate.

class AuthorCreate(PermissionRequiredMixin, CreateView):
    model = Author
    fields = '__all__'
    initial={'date_of_death':'12/10/2016',}
    permission_required = 'catalog.can_mark_returned'

Помните, — вам надо проверить все, что касается вашего кода, или структуры. Это включает в себя: кто имеет доступ к отображению, начальную дату, применяемый шаблон, а также перенаправление из отображения в случае успеха.

Итоги

Написание тестов не является ни весельем, ни развлечением и, соответственно, при создании сайтов часто остаётся напоследок (или вообще не используется). Но тем не менее, они являются действенным механизмом, который позволяет вам убедиться, что ваш код в находится безопасности, даже если в него добавляются какие-либо изменения. Кроме того, тесты повышают эффективность поддержки вашего кода.

В данном руководстве мы продемонстрировали вам принципы написания тестов для ваших моделей, форм и отображений. Мы кратко перечислили что именно необходимо тестировать, что обычно сложно выявить в самом начале разработки. Существует много аспектов которые необходимо изучить, но даже с тем что мы уже узнали, вы имеете возможность создавать эффективные юнит-тесты для значительного улучшения процесса разработки.

Следующая и последняя часть руководства покажет вам как запустить ваш чудесный (и полностью протестированный!) веб-сайт Django.

Смотрите также

Django comes with a useful set of utilities for testing
various aspects of web apps. For example, its TestCase and TransactionTestCase
base classes as well as its test client make it easier to test those cases.

However the Django testing utilities can’t cover everything, so there are many cases
where a robust test involves a lot more manual work than ideal. One of these cases that
crops up pretty often is in testing form and model validation, and in particular,
asserting exactly which validation errors happen.

Suppose we have the following model:

from django import models

class Person(models.Model):
    name = models.CharField(max_length=100)
    dob = models.DateField()
    website = models.URLField(required=False)

    def clean(self):
        if self.name == 'Joe' and self.website is None:
            raise ValidationError({
                'website': "Joe must have a website"
            })

If we want to test that our custom validation is done correctly, we might try
to use assertRaises and do the following:

from django.core.validation import ValidationError
from django.test import TestCase

class PersonTest(TestCase):

    def test_joe_must_have_a_website(self):
        p = Person(name='Joe')
        with self.assertRaises(ValidationError):
            p.full_clean()

This test contains a bug. Can you spot it? The problem is that our model has additional
required fields (dob in our case) which we haven’t specified in the test, so it will
always raise a validation error, even if our code is not correct.

If this was a Form test, we might use assertFormError. For model tests, there’s no
such shortcut so we’re forced to go about it manually:

from django.core.validation import ValidationError
from django.test import TestCase

class PersonTest(TestCase):

    def test_joe_must_have_a_website(self):
        p = Person(name='Joe', dob=datetime.date(1990, 1, 1))

        try:
            p.full_clean()
        except ValidationError as e:
            self.assertTrue('name' in e.message_dict)

If you have a lot of model-level validation, it can be cumbersome to manually use this
pattern all the time. Instead, you can extract the pattern into a helper:

class ValidationErrorTestMixin(object):

    @contextmanager
    def assertValidationErrors(self, fields):
        """
        Assert that a validation error is raised, containing all the specified
        fields, and only the specified fields.
        """
        try:
            yield
            raise AssertionError("ValidationError not raised")
        except ValidationError as e:
            self.assertEqual(set(fields), set(e.message_dict.keys()))

You can then use it as:

class PersonTest(ValidationErrorTestMixin, TestCase):

    def test_joe_must_have_a_website(self):
        p = Person(name='Joe', dob=datetime.date(1990, 1, 1))

        with self.assertValidationErrors(self, ['name']):
            p.full_clean()

In cases where you raise validation errors that are not specific to a field, you
can check for NON_FIELD_ERRORS:

from django.core.exceptions import NON_FIELD_ERRORS

class Person(models.Model):
    name = models.CharField(max_length=100)
    dob = models.DateField()
    website = models.URLField(required=False)

    def clean(self):
        if self.name == 'Joe' and self.website is None:
            raise ValidationError("Joe must have a website")


class PersonTest(ValidationErrorTestMixin, TestCase):

    def test_joe_must_have_a_website(self):
        p = Person(name='Joe', dob=datetime.date(1990, 1, 1))

        with self.assertValidationErrors(self, [NON_FIELD_ERRORS]):
            p.full_clean()

If you do a lot of model-level validation, extracting the validation pattern
into assertValidationError as shown here will make your tests more robust,
easier to write, more readable and maintainable.

Author

Senko Rašić

We’re small, experienced and passionate team of web developers, doing custom app development and web consulting.

Тестирование приложений Django¶

Автоматическое тестирование является очень полезной методикой для современного веб разработчика. Вы можете использовать коллекцию тестов (пакет тестов) для решения, или исключения, ряда проблем:

  • Когда вы пишете новый код, вы можете использовать тесты для проверки того, что ваш код работает как ожидалось.

  • Когда вы рефакторите или модифицируете старый код, вы можете использовать тесты, чтобы гарантировать неизменность поведения вашего приложения после внесения изменений.

Тестирование веб приложений – это сложная задача, так как веб приложение создаётся с использованием нескольких слоёв логики: от HTTP уровня для обработки запросов до проверки и обработки форм, отображения шаблонов. Используя механизм Django для запуска тестов и дополнительные утилиты, вы можете имитировать запросы, вставлять тестовые данные, инспектировать вывод вашего приложения и в общем проверять, что ваш код делает именно то, что должен.

Ну и самое главное, всё это несложно.

Этот документ разбит на две основные части. В первой мы рассказываем как писать тесты для Django. Во второй мы объясняем как их использовать.

Создание тестов¶

Существует два основных способа создания тестов для Django, в соответствии с двумя тестовыми фреймворками, которые поставляются в стандартной библиотеке языка Python. Рассмотрим их:

  • Юнит тесты – тесты, которые представлены в виде методов класса, унаследованного от unittest.TestCase или от django.tests.TestCase. Пример:

    import unittest
    
    class MyFuncTestCase(unittest.TestCase):
        def testBasic(self):
            a = ['larry', 'curly', 'moe']
            self.assertEqual(my_func(a, 0), 'larry')
            self.assertEqual(my_func(a, 1), 'curly')
    
  • Док тесты – тесты, которые встраиваются в описание ваших функций и написаны в стиле эмуляции сессии интерактивного интерпретатора языка Python. Пример:

    def my_func(a_list, idx):
        """
        >>> a = ['larry', 'curly', 'moe']
        >>> my_func(a, 0)
        'larry'
        >>> my_func(a, 1)
        'curly'
        """
        return a_list[idx]
    

Мы обсудим выбор соответствующего фреймворка чуть позже. Но сразу скажем, что опытные разработчики предпочитают юнит тесты. Вы можете также использовать любые другие тестовые фреймворки для Python, мы про это тоже расскажем.

Создание юнит тестов¶

Юнит тесты Django используют модуль unittest стандартной библиотеки языка Python. Этот модуль определяет тесты в виде классов.

unittest2

Изменено в Django 1.3.

Python 2.7 привнёс достаточно серьёзные изменения в библиотеку юнит тестов, добавив очень полезные возможности. Чтобы дать возможность каждому Django проекту использовать эти новые возможности, Django поставляется с копией unittest2 из Python 2.7, спортированной для работы с Python 2.5.

Django предоставляет модуль django.utils.unittest для доступа к этой библиотеке. Если вы используете Python 2.7 или если вы установили unittest2 локально, Django будет использовать оригинальную версию библиотеки. В остальных случаях Django будет использовать свою версию библиотеки.

Для использования этого модуля делайте так:

from django.utils import unittest

там где вы раньше использовали:

Если вы желаете продолжить использовать базовую библиотеку unittest, то продолжайте. Вы просто не получите доступ к новым возможностям unittest2.

В приложении есть два места, которые проверяет test runner при запуске юнит тестов:

  • Файл models.py. Тест раннер ищет наследников класса unittest.TestCase в этом модуле.

  • Файл и именем tests.py в каталоге приложения, т.е. в каталоге, где находится файл models.py. И снова, тест раннер ищет наследников класса unittest.TestCase в этом модуле.

Ниже представлен пример такого класса:

from django.utils import unittest
from myapp.models import Animal

class AnimalTestCase(unittest.TestCase):
    def setUp(self):
        self.lion = Animal.objects.create(name="lion", sound="roar")
        self.cat = Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        self.assertEqual(self.lion.speak(), 'The lion says "roar"')
        self.assertEqual(self.cat.speak(), 'The cat says "meow"')

При выполнении ваших тестов, обычным поведением тестовой утилиты будет поиск всех тестов (т.е., потомков класса unittest.TestCase) в файлах models.py и tests.py, автоматическое построение тестового набора и выполнение этого набора.

Есть ещё один способ определения тестового набора для модуля: если в models.py или tests.py«будет определена функция «suite(), то тест раннер будет использовать эту функцию для создания набора тестов для этого модуля. Это поведение соответствует договорённостям об организации для юнит тестов. Обратитесь к документации языка Python для получения информации о том, как создавать сложные тестовые наборы.

Для получения информации по unittest, обратитесь к документации на язык Python.

Создание доктестов¶

Доктесты используют стандартный модуль doctest языка Python, который выполняет поиск встроенной документации для элементов модуля, в которых имитируется сессия интерактивного интерпретатора. Подробное объяснение функционала doctest не входит в цели данного документа. Обратитесь к официальной документации на язык Python.

What’s a docstring?

Хорошее описание встроенной документации (docstrings) (и несколько инструкций по их эффективному использованию) можно найти в PEP 257:

Встроенная документация – это строка, которая находится на месте первого оператора в определении модуля, функции, класса или метода. Такая документация доступна через свойство __doc__ объекта.

Например, эта функция имеет встроенную документацию, которая объясняет её предназначение:

def add_two(num):
    "Return the result of adding two to the provided number."
    return num + 2

Так как тесты часто улучшают документацию, размещение тестов прямо во встроенной документации является эффективным способом документирования и тестирования вашего кода.

Аналогично юнит тестам, в случае приложения Django, тест раннер ищет встроенную документацию в двух местах:

  • Файл models.py. Вы можете определить встроенную документацию для модуля и/или встроенную документацию для отдельных моделей. Обычно на уровне модуля размещают тесты уровня приложения, а на уровне моделей тесты описывают во встроенной документации моделей.

  • В файде tests.py в каталоге приложения, т.е., в каталоге, где находится файл models.py. Этот файл является основным местом для всех доктестов, которые не относятся к моделям.

Этот пример аналогичен примеру из раздела юниттестов:

# models.py

from django.db import models

class Animal(models.Model):
    """
    An animal that knows how to make noise

    # Create some animals
    >>> lion = Animal.objects.create(name="lion", sound="roar")
    >>> cat = Animal.objects.create(name="cat", sound="meow")

    # Make 'em speak
    >>> lion.speak()
    'The lion says "roar"'
    >>> cat.speak()
    'The cat says "meow"'
    """
    name = models.CharField(max_length=20)
    sound = models.CharField(max_length=20)

    def speak(self):
        return 'The %s says "%s"' % (self.name, self.sound)

При запуске ваших тестов, тест раннер найдёт эту встроенную документацию (обратите внимание на то, что часть её выглядит как журнал сессии интерактивного интерпретатора Python) и выполнит эти строки, проверяя совпадение результатов.

При тестировании моделей, следует отметить, что тест раннер обеспечивает создание тестовой базы данных. Следовательно, любой тест, который взаимодействует с базой данных (например, создание и сохранение экземпляров модели) никак не повлияет на вашу базу данных. Тем не менее, тестовая база данных не обновляется при работе доктестов. Таким образом, если доктест требует определённого состояния базы данных, вы должны очистить базу или подгрузить фикстуры. (Обратитесь далее к разделу с описанием фикстур для получения подробной информации.) Следует отметить, что для использования данной возможности, пользователь базы данных, который используется Django, должен обладать правом CREATE DATABASE.

Для получения подробной информации о doctest обратитесь к документации на язык Python.

Что должен выбрать я?¶

Так как Django поддерживает оба стандартных тестовых фреймворка языка Python, выбор одного из них лежит на вас. Вы можете даже использовать *оба” одновременно.

Тем не менее, новичкам трудно сделать такой выбор. Поэтому мы приводим список ключевых различий, которые могут помочь вам сделать правильный выбор:

  • Если вы работали с Python достаточно продолжительное время, то doctest возможно будет более “питонским”. Он был создан, чтобы сделать написание тестов максимально простым, он не требует написания классов или методов. Вы просто располагаете тесты во встроенной документации (и корректируете саму документацию). Однако, доктесты хороши для простого кода, но не подходят для случая, когда вам надо создать либо сложные тесты или качественную документацию. Сбои во время тестов часто бывает непросто отладить, так как не всегда бывает очевидна причина. Таким образом, доктесты следует применять только для документирования примеров.

  • Библиотека unittest возможно будет более понятна для разработчиков, которые пришли из мира Java. Библиотека unittest была создана по примеру JUnit, что упрощает процесс вхождения для людей, имевших дело с любым тестовым фреймворком на его основе.

  • Если вам надо написать ряд тестов, которые используют одинаковый код, то вам понравится организация фреймворка unittest по классам и методам. Такой подход упрощает абстракцию общих задач в общие методы. Фреймвор также поддерживает явные процедуры настройки и очистки, которые предоставляют высокий уровень контроля над средой, в который происходит выполнение тестов.

  • Если вы пишете тесты для кода Django, то вы должны использовать unittest.

Выполнение тестов¶

Написав тесты, запустите их с помощью команды test утилиты manage.py вашего проекта:

По умолчанию, эта команда запустит все тесты каждого приложения, которые перечислены в параметре конфигурации INSTALLED_APPS. Если требуется запустить тесты только одного определённого приложения, то добавьте имя этого приложения в командную строку. Например если параметр конфигурации INSTALLED_APPS содержит ‘myproject.polls’ и ‘myproject.animals’, вы можете выполнить юниттесты из myproject.animals с помощью команды:

$ ./manage.py test animals

Следует отметить, что мы использовали animals, а не myproject.animals.

Вы даже можете указать конкретный тест для запуска. Для запуска одного теста из приложения (например, AnimalTestCase, который описан в разделе Создание юниттестов <writing unit tests>`_), добавите имя этого теста к имени приложения в командной строке:

$ ./manage.py test animals.AnimalTestCase

Можно пойти ещё дальше! Для запуска единственного метода из теста, добавьте его имя в командную строку:

$ ./manage.py test animals.AnimalTestCase.test_animals_can_speak

Вы можете использовать те же правила и при использовании доктестов. Django использует тестовую метку в качестве пути к методу или классу, который вы желаете запустить. Если файлы models.py или tests.py приложения содержат функцию со встроенной документацией или класс с документацией, вы можете вызвать эти тесты, добавив имя метода или класса к метке:

$ ./manage.py test animals.classify

Если требуется запустить доктест для определённого метода в классе, добавьте имя метода к метке:

$ ./manage.py test animals.Classifier.run

Если вы используете словарь __test__ для определения доктестов для модуля, Django будет использовать метку как ключ словаря __test__ для определённых models.py и tests.py.

Если вы нажмёте Ctrl-C во время работы тестов, то раннер дождётся завершения работы текущего теста и аккуратно завершит свою работу. При завершении своей работы раннер выведет всю информацию о найденных ошибках, укажет сколько тестов было выполнено и как много ошибок и сбоев было обнаружено. Как обычно, в конце раннер удалит все тестовые базы данных. Таким образом, нажатие Ctrl-C может быть очень полезным, если вы забыли передать аргумент —failfast. Следует отметить, что некоторые тесты могут падать и это комбинация клавиш позволяет получить информацию от тестов, не дожидаясь завершения всего набора.

Если вам не надо дожидаться окончания работы текущего теста, вы можете нажать Ctrl-C во второй раз и тест будет прерван немедленно, без очистки. Никакой информации о пройденных тестах не будет выведено. Тестовые базы данных останутся нетронутыми.

Test with warnings enabled

Хорошей идеей будет запуск ваших тестов с включенным функционалом уведомлений языка Python: python -Wall manage.py test. Флаг -Wall указывает Python, что надо отображать напоминаний при использовании устаревшего функционала. Django, как и многие другие библиотеки языка Python, используют такие напоминания, чтобы уведомить пользователей об устаревшем функционале. Также этот механизм может помечать части вашего кода, которые не то, чтобы неправильные, но могли бы стать лучше.

Выполнение тестов без тест раннера¶

Если вам надо выполнять тесты без запуска ./manage.py test (например, из консоли), вам надо настроить тестовое окружение. Django предоставляет для этого удобный метод:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

Этот метод создаёт тестовую базу данных и переводит функционал Django в режимы, которые позволяют проводить повторяемое тестирование.

Вызов метода setup_test_environment() выполняется автоматически во время работы ./manage.py test. Без использования тест раннера вам придётся вызывать этот метод вручную.

Тестовая база данных¶

Тесты, которым необходима база данных (так называемые модельные тесты), не используют вашу “реальную” (продакшн) базу данных. Для тестов создаётся отдельная база данных.

Независимо от того, прошли тесты успешно или нет, тестовая база данных уничтожается в конце процесса тестирования

По умолчанию, тестовые базы данных получают свои имена, добавляя префикс test_ к значению параметра NAME баз данных, который определён в параметре конфигурации DATABASES. При использовании SQLite тесты по умолчанию будут использовать оперативную базу данных (т.е., база данных будет создана в оперативной памяти, диск вообще не используется!) Если вам надо использовать собственное имя для тестовой базы данных, определите TEST_NAME для нужной базы данных в параметре конфигурации DATABASES.

Во остальном, тест раннер будет использовать оригинальные настройки для базы данных: ENGINE, USER, HOST и так далее. Тестовая база данных создаётся от имени пользователя, который определён в USER, так чтовам потребуется дать ему соответствующие права, чтобы он мог это сделать.

Для точного контроля кодировкой символов в вашей тестовой базе данных, используйте параметр TEST_CHARSET. При использовании MySQL вы можете также использовать параметр TEST_COLLATION для управления сопоставлениями, используемыми базой данных. Обратитесь к документации по параметрам конфигурации проекта для получения информации по этим дополнительным параметрам.

Тестирование конфигураций мастер/ведомый¶

Добавлено в Django 1.2.

При тестировании конфигураций со множество баз данных с репликацией вида мастер/ведомый, такая стратегия создания тестовых баз данных приводит к проблеме. После создания тестовых баз данных никакой репликации выполнено не будет, соответственно, данные, созданные на мастере, не появятся на ведомой базе.

Для решения этой задачи Django позволяет вам определить, что база данных является тестовым зеркалом. Рассмотрим следующий (упрощённый) пример конфигурации базы данных:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject',
        'HOST': 'dbmaster',
         # ... plus some other settings
    },
    'slave': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject',
        'HOST': 'dbslave',
        'TEST_MIRROR': 'default'
        # ... plus some other settings
    }
}

В такой конфигурации у нас есть два сервера баз данных: dbmaster, имеющий псевдоним default, и dbslave с псевдонимом slave. Как вы могли ожидать, dbslave настроен администратором баз данных как копия dbmaster. Таким образом, любая запись данных в default приведёт к их появлению на slave.

Если Django создаст две независимые тестовые базы данных, это приведёт к сбою любых тестов, которые ожидают проведения репликации. Однако, база данных slave настрена как тестовое зеркало (с помощью параметра конфигурации TEST_MIRROR), т.е., во время тестирования slave должен рассматриваться как зеркало default.

При настройке тестового окружения, тестовая версия slave не будет создана. Вместо этого, соединение к slave будет перенаправлено на default. В результате, запись в default будет приводить к появлению данных на slave. Но лишь потому что это одна и та же база данных, а не из-за репликации между двумя базами данных.

Управление порядком создания тестовых баз данных¶

Добавлено в Django 1.3.

По умолчанию, Django всегда создаёт базу данных default первой. Однако никак не гарантирует порядок создания остальных баз данных вашей конфигурации.

Если конфигурация ваших баз данных требует определённого порядка создания баз данных, вы можете указать эти зависимости с помощью параметра конфигурации TEST_DEPENDENCIES. Рассмотрим следующий (упрощенный) пример конфигурации баз данных:

DATABASES = {
    'default': {
         # ... db settings
         'TEST_DEPENDENCIES': ['diamonds']
    },
    'diamonds': {
        # ... db settings
    },
    'clubs': {
        # ... db settings
        'TEST_DEPENDENCIES': ['diamonds']
    },
    'spades': {
        # ... db settings
        'TEST_DEPENDENCIES': ['diamonds','hearts']
    },
    'hearts': {
        # ... db settings
        'TEST_DEPENDENCIES': ['diamonds','clubs']
    }
}

Используя эту конфигурацию, база данных diamonds будет создана первой, так как только у неё нет зависимостей. Базы данных default и clubs будут созданы далее (хотя порядок создания этой пары случаен). Затем будет создана hearts и в конце spades базы данных.

Если в параметре TEST_DEPENDENCIES будет определена циклическая зависимость, то будет вызвано исключение ImproperlyConfigured.

Другие условия тестирования¶

Независимо от значения параметра конфигурации DEBUG, Django выполняет все тесты, устанавливая DEBUG=False. Так делается для того, чтобы проверять код в условиях, аналогичных боевым.

Изучение вывода тестов¶

При запуске тестов, вы увидите ряд сообщений тест раннера. Вы можете управлять уровнем детализации этих сообщений с помощью аргумента verbosity командной строки:

Creating test database...
Creating table myapp_animal
Creating table myapp_mineral
Loading 'initial_data' fixtures...
No fixtures found.

Это показывает вам, что тест раннер создал тестовую базу данных, как мы об этом рассказали в предыдущем разделе.

После создания тестовой базы данных, Django запускает ваши тесты. Если всё идёт без ошибок, то вы увидите подобный вывод:

----------------------------------------------------------------------
Ran 22 tests in 0.221s

OK

Если во время выполнения тестов произошли ошибки, то вы увидите полную информацию по непройденным тестам:

======================================================================
FAIL: Doctest: ellington.core.throttle.models
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/django/test/doctest.py", line 2153, in runTest
    raise self.failureException(self.format_failure(new.getvalue()))
AssertionError: Failed doctest test for myapp.models
  File "/dev/myapp/models.py", line 0, in models

----------------------------------------------------------------------
File "/dev/myapp/models.py", line 14, in myapp.models
Failed example:
    throttle.check("actor A", "action one", limit=2, hours=1)
Expected:
    True
Got:
    False

----------------------------------------------------------------------
Ran 2 tests in 0.048s

FAILED (failures=1)

Полный разбор этого вывода ошибки находится вне данного документа, но тест ошибки говорит сам за себя. Вы можете обратиться к документации на библиотеку unittest языка Python для получения подробной информации.

Следует отметить, что тест раннер возвращает 1 для любого количества ошибок. Если все тесты пройдены, будет возвращен 0. Эта особенность полезна в случае, когда вам надо запускать раннер из скриптов шелла, проверяя результат его работы.

Using different testing frameworks¶

Clearly, doctest and unittest are not the only Python testing
frameworks. While Django doesn’t provide explicit support for alternative
frameworks, it does provide a way to invoke tests constructed for an
alternative framework as if they were normal Django tests.

When you run ./manage.py test, Django looks at the TEST_RUNNER
setting to determine what to do. By default, TEST_RUNNER points to
‘django.test.simple.DjangoTestSuiteRunner’. This class defines the default Django
testing behavior. This behavior involves:

  1. Performing global pre-test setup.
  2. Looking for unit tests and doctests in the models.py and
    tests.py files in each installed application.
  3. Creating the test databases.
  4. Running syncdb to install models and initial data into the test
    databases.
  5. Running the unit tests and doctests that are found.
  6. Destroying the test databases.
  7. Performing global post-test teardown.

If you define your own test runner class and point TEST_RUNNER at
that class, Django will execute your test runner whenever you run
./manage.py test. In this way, it is possible to use any test framework
that can be executed from Python code, or to modify the Django test execution
process to satisfy whatever testing requirements you may have.

Defining a test runner¶

A test runner is a class defining a run_tests() method. Django ships
with a DjangoTestSuiteRunner class that defines the default Django
testing behavior. This class defines the run_tests() entry point,
plus a selection of other methods that are used to by run_tests() to
set up, execute and tear down the test suite.

class DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=True, **kwargs

verbosity determines the amount of notification and debug information
that will be printed to the console; 0 is no output, 1 is normal
output, and 2 is verbose output.

If interactive is True, the test suite has permission to ask the
user for instructions when the test suite is executed. An example of this
behavior would be asking for permission to delete an existing test
database. If interactive is False, the test suite must be able to
run without any manual intervention.

If failfast is True, the test suite will stop running after the
first test failure is detected.

Django will, from time to time, extend the capabilities of
the test runner by adding new arguments. The **kwargs declaration
allows for this expansion. If you subclass DjangoTestSuiteRunner or
write your own test runner, ensure accept and handle the **kwargs
parameter.

Добавлено в Django 1.4.

Your test runner may also define additional command-line options.
If you add an option_list attribute to a subclassed test runner,
those options will be added to the list of command-line options that
the test command can use.

Attributes¶

DjangoTestSuiteRunner.option_list¶

Добавлено в Django 1.4.

This is the tuple of optparse options which will be fed into the
management command’s OptionParser for parsing arguments. See the
documentation for Python’s optparse module for more details.

Methods¶

DjangoTestSuiteRunner.run_tests(test_labels, extra_tests=None, **kwargs

Run the test suite.

test_labels is a list of strings describing the tests to be run. A test
label can take one of three forms:

  • app.TestCase.test_method – Run a single test method in a test
    case.
  • app.TestCase – Run all the test methods in a test case.
  • app – Search for and run all tests in the named application.

If test_labels has a value of None, the test runner should run
search for tests in all the applications in INSTALLED_APPS.

extra_tests is a list of extra TestCase instances to add to the
suite that is executed by the test runner. These extra tests are run
in addition to those discovered in the modules listed in test_labels.

This method should return the number of tests that failed.

DjangoTestSuiteRunner.setup_test_environment(**kwargs

Sets up the test environment ready for testing.

DjangoTestSuiteRunner.build_suite(test_labels, extra_tests=None, **kwargs

Constructs a test suite that matches the test labels provided.

test_labels is a list of strings describing the tests to be run. A test
label can take one of three forms:

  • app.TestCase.test_method – Run a single test method in a test
    case.
  • app.TestCase – Run all the test methods in a test case.
  • app – Search for and run all tests in the named application.

If test_labels has a value of None, the test runner should run
search for tests in all the applications in INSTALLED_APPS.

extra_tests is a list of extra TestCase instances to add to the
suite that is executed by the test runner. These extra tests are run
in addition to those discovered in the modules listed in test_labels.

Returns a TestSuite instance ready to be run.

DjangoTestSuiteRunner.setup_databases(**kwargs

Creates the test databases.

Returns a data structure that provides enough detail to undo the changes
that have been made. This data will be provided to the teardown_databases()
function at the conclusion of testing.

DjangoTestSuiteRunner.run_suite(suite, **kwargs

Runs the test suite.

Returns the result produced by the running the test suite.

DjangoTestSuiteRunner.teardown_databases(old_config, **kwargs

Destroys the test databases, restoring pre-test conditions.

old_config is a data structure defining the changes in the
database configuration that need to be reversed. It is the return
value of the setup_databases() method.

DjangoTestSuiteRunner.teardown_test_environment(**kwargs

Restores the pre-test environment.

DjangoTestSuiteRunner.suite_result(suite, result, **kwargs

Computes and returns a return code based on a test suite, and the result
from that test suite.

Testing utilities¶

To assist in the creation of your own test runner, Django provides a number of
utility methods in the django.test.utils module.

setup_test_environment()¶

Performs any global pre-test setup, such as the installing the
instrumentation of the template rendering system and setting up
the dummy SMTPConnection.

teardown_test_environment()¶

Performs any global post-test teardown, such as removing the black
magic hooks into the template system and restoring normal email
services.

The creation module of the database backend (connection.creation)
also provides some utilities that can be useful during testing.

create_test_db([verbosity=1, autoclobber=False]

Creates a new test database and runs syncdb against it.

verbosity has the same behavior as in run_tests().

autoclobber describes the behavior that will occur if a
database with the same name as the test database is discovered:

  • If autoclobber is False, the user will be asked to
    approve destroying the existing database. sys.exit is
    called if the user does not approve.
  • If autoclobber is True, the database will be destroyed
    without consulting the user.

Returns the name of the test database that it created.

create_test_db() has the side effect of modifying the value of
NAME in DATABASES to match the name of the test
database.

destroy_test_db(old_database_name[, verbosity=1]

Destroys the database whose name is the value of NAME in
DATABASES, and sets NAME to the value of
old_database_name.

The verbosity argument has the same behavior as for
DjangoTestSuiteRunner.

In the previous Understand Django article, we saw how static files like CSS, JavaScript, and images can be incorporated into your site. Now we’re going to focus on how to verify that your website works and continues to work by writing automated tests that check your pages and your code logic.

Why Write Tests

I’m going to assume that if you’re reading this, then you’ve either got a Django project or are considering working with Django to build a project. If that’s true, think about your project and how you would make sure it works.

When you start out with a project, whether for a tutorial or for something real that you plan to grow, the fledgling site has very little functionality. To check that the site is working, you can start up the local web server, open your browser, navigate to the localhost URL, and confirm that the site is functional. How long does that take? 5 seconds? 15 seconds? 30 seconds?

For starting out, manually checking out your site is fine. What happens, though, when your create more pages? How do you continue to confirm that all your pages are functional? You could open up the local site and start clicking around, but the time spent confirming that everything works begins to grow. Maybe your verification effort takes 3 minutes, 5 minutes, or perhaps much more. If you’re not careful, your creation may start to feel like the mythical multi-headed Hydra, and what once was a fun project to work on devolves into a chore of tedious page verification.

You can’t eliminate the fact that a larger project means that there is more to check. What you can do is change the name of the game. You can change your page checking from something manual that may take 15 seconds to verify a page to something that a computer can do in milliseconds.

This is where automated tests come into the picture. Automated tests let computers do what computers do best: run repetitive tasks repeatedly, consistently, and quickly. When we write tests, our goal is to confirm some logic or behavior in a deterministic way.

Let’s look at a test for a hypothetical add function which functions like the + operator. This should give us a feel for what an automated test is like if you’ve never encountered tests before.

def test_does_it_add():
    assert add(40, 2) == 42

Enter fullscreen mode

Exit fullscreen mode

The test works by running the code and comparing the result to whatever we expect that result to be. The test asserts that the equality statement is true. If the equality is false, then the assertion raises an exception and the test fails.

This automated test would take virtually no time to run if you compared it to running the function in a Python REPL to inspect the result manually.

Seeing a silly example of an add function doesn’t really help you much with how you should test your Django project. Next, we’ll look at some types of tests for Django. If you add these kinds of tests to your project, you’ll be able to make changes to your website with more confidence that you’re not breaking things.

Useful Types Of Django Tests

When we explored the anatomy of a Django application, I noted that I always delete the tests.py file that comes with the startapp command. The reason I do this is because there are different kinds of tests, and I want those different kinds to live in separate files. My apps have those separate files in a tests package within the app instead of a tests.py module.

My tests package will often mirror the structure of the application itself. The program which executes tests, which is called a «test runner,» typically expects to find tests in files that start with test_. The package often includes:

  • test_forms.py
  • test_models.py
  • test_views.py
  • etc.

This structure hints at the kinds of tests that you’d write for your application, but I’ll touch on specifics more a bit later. Broadly, when we write automated tests, there is an important dimension to consider: how much application code should my test run?

The answer to that question influences the behavior of tests. If we write a test that runs a lot of code, then we benefit by checking a lot of a system at once; however, there are some downsides:

  • Running a lot of code means more things can happen and there is a higher chance of your test breaking in unexpected ways. A test that often breaks in unexpected ways is called a «brittle» test.
  • Running a lot of code means that there is a lot of code to run. That’s axiomatic, but the implication is that a test with more code to execute will take longer to run. Big automated tests are still very likely to be much faster than the same test executed manually, so running time is relative.

When we have tests that runs many parts of your application that are integrated together, we call these tests integration tests. Integration tests are good at surfacing issues related to the connections between code. For instance, if you called a method and passed in the wrong arguments, an integration test is likely to discover that problem.

On the other end of the spectrum are tests that run very little code. The add test from above is a good example. These kinds of tests check individual units of code (e.g., a Django model). For that reason, we call these unit tests. Unit tests are good at checking a piece of code in isolation to confirm its behavior.

Unit tests have downsides too. These tests execute without a lot of context from the rest of an application. This can help you confirm the behavior of the piece, but it might not be the behavior that the larger application requires.

In this explanation, the lesson is that both kinds of tests are good, yet have tradeoffs. Beware of anyone who tells you that you should only write one kind of test or the other.

A good set of automated tests will include both unit and integration tests to check behavior of the individual units and the interconnections between parts.

We have to consider another aspect to this discussion: what is the «right» amount of code for a unit test? There’s no absolutely correct answer here. In fact, this topic is hotly debated among testers.

Some people will assert that a unit test should only run the code for that unit. If you have a class that implements some pure logic and doesn’t need other code, then you’re in the ideal case. But what happens if you’re testing a method that you added to a Django model that needs to interact with a database? Even if the only thing you’re testing is the individual model method, a unit test purist would highlight that the test is actually an integration test if it interacts with a database.

I usually find this kind of discussion counterproductive. In my experience, this sort of philosophical debate about what is a unit test doesn’t typically help with testing your web app to verify its correctness. I brought all of this up because, if you’re going to learn more about testing after this article, I caution you to avoid getting sucked into this definition trap.

Here are my working definitions of unit and integration tests in Django. These definition are imperfect (as are any definitions), but they should help frame the discussion in this article.

  • Unit tests — Tests that check individual units within a Django project like a model method or a form.
  • Integration test — Tests that check a group of units and their interactions like checking if a view renders the expected output.

Now that we have some core notion of what tests are about, let’s get into the details.

Unit Tests

As we get into some examples, I need to introduce a couple of tools that I use on all of my Django projects. I’ll describe these tools in more depth in a later section, but they need a brief introduction here or my examples won’t make much sense. My two «must have» packages are:

  • pytest-django
  • factory-boy

pytest-django is a package that makes it possible to run Django tests through the pytest program. pytest is an extremely popular Python testing tool with a huge ecosystem of extensions. In fact, pytest-django is one of those extensions.

My biggest reason for using pytest-django is that it let’s me use the assert keyword in all of my tests. In the Python standard library’s unittest module and, by extension, Django’s built-in test tools which subclasses unitttest classes, checking values requires methods like assertEqual and assertTrue. As we’ll see, using the assert keyword exclusively is a very natural way to write tests.

The other vital tool in my tool belt is factory-boy. factory_boy is a tool for building test database data. The library has fantastic Django integration and gives us the ability to generate model data with ease.

Again, I’ll focus on these two packages later on to cover more of their features, but you’ll see them used immediately in the examples.

Model Tests

In Django projects, we use models to hold data about our app, so it’s very natural to add methods to the models to interact with the data. How do we write a test that checks that the method does what we expect?

I’m going to give you a mental framework for any of your tests, not only unit tests. This framework should help you reason through any tests that you encounter when reading and writing code. The framework is the AAA pattern. The AAA patterns stands for:

  • Arrange — This is the part of the test that sets up your data and any necessary preconditions for your test.
  • Act — This stage is when your test runs the application code that you want to test.
  • Assert — The last part checks that your action is what you expected.

For a model test, this looks like:

# application/tests/test_models.py

from application.models import Order
from application.tests.factories import OrderFactory

class TestOrder:
    def test_shipped(self):
        """After shipping an order, the status is shipped."""
        order = OrderFactory(status=Order.Status.PENDING)

        order.ship()

        order.refresh_from_db()
        assert order.status == Order.Status.SHIPPED

Enter fullscreen mode

Exit fullscreen mode

We can imagine a project that includes an ecommerce system. A big part of handling orders is tracking status. We could manually set the status field throughout the app, but changing status within a method gives us the chance to do other things. For instance, maybe the ship method also triggers sending an email.

In the test above, we’re checking the state transition from PENDING to SHIPPED. The test acts on the ship method, then refreshes the model instance from the database to ensure that the SHIPPED status persisted.

What are some good qualities about this test?

The test includes a docstring. Trust me, you will benefit from docstrings on your tests. There is a strong temptation to leave things at test_shipped, but future you may not have enough context.

Many developers opt for long test names instead. While I have no problem with long descriptive test names, docstrings are helpful too. Whitespace is a good thing and, in my opinion, it’s easier to read «The widget updates the game state when pushed.» than test_widget_updates_game_state_when_pushed.

The test checks one action. A test that checks a single action can fit in your head. There’s no question about interaction with other parts. There’s also no question about what is actually being tested. The simplicity of testing a single action makes each unit test tell a unique story.

Conversely, you’ll likely encounter tests in projects that do a lot of initial arrangement, then alternate between act and assert lines in a single test. These kinds of tests are brittle (i.e., the term to indicate that the test can break and fail easily) and are difficult to understand when there is a failure.

The qualities in this test translate to lots of different test types. I think that’s the beauty of having a solid mental model for testing. Once you see the way that tests:

  1. Set up the inputs.
  2. Take action.
  3. Check the outputs.

Then automated testing becomes a lot less scary and more valuable to you. Now let’s see how this same pattern plays out in forms.

Form Tests

When writing tests, we often want to write a «happy path» test. This kind of test is when everything works exactly as you hope. This is a happy path form test.

# application/tests/test_forms.py

from application.forms import SupportForm
from application.models import SupportRequest

class TestSupportForm:
    def test_request_created(self):
        """A submission to the support form creates a support request."""
        email = "hello@notreal.com"
        data = {
            "email": email, "message": "I'm having trouble with your product."
        }
        form = SupportForm(data=data)
        form.is_valid()

        form.save()

        assert SupportRequest.objects.filter(email=email).count() == 1

Enter fullscreen mode

Exit fullscreen mode

With this test, we are synthesizing a POST request. The test:

  • Builds the POST data as data
  • Creates a bound form (i.e., connects data=data in the constructor)
  • Validates the form
  • Saves the form
  • Asserts that a new record was created

Notice that I’m bending the AAA rules a bit for this test. Part of the Django convention for forms is that the form is valid before calling the save method. If that convention is not followed, then cleaned_data won’t be populated correctly and most save methods depend on cleaned_data. Even though is_valid is an action, I view it as a setup step for form tests.

When we work with forms, a lot of what we care about is cleaning the data to make sure that junk is not getting into your app’s database. Let’s write a test for an invalid form.

# application/tests/test_forms.py

from application.forms import SupportForm
from application.models import SupportRequest

class TestSupportForm:
    # ... def test_request_created ...

    def test_bad_email(self):
        """An malformed email address is invalid."""
        data = {"email": "bogus", "message": "Whatever"}
        form = SupportForm(data=data)

        is_valid = form.is_valid()

        assert not is_valid
        assert 'email' in form.errors

Enter fullscreen mode

Exit fullscreen mode

The test shows the mechanics for checking an invalid form. The key elements are:

  • Set up the bad form data
  • Check the validity with is_valid
  • Inspect the output state in form.errors

This test shows how to check an invalid form, but I’m less likely to write this particular test in a real project. Why? Because the test is checking functionality from Django’s EmailField which has the validation logic to know what is a real email or not.

Generally, I don’t think it’s valuable to test features from the framework itself. A good open source project like Django is already testing those features for you. When you write form tests, you should check on custom clean_* and clean methods as well as any custom save method that you might add.

The patterns for both happy path and error cases are what I use for virtually all of my Django form tests. Let’s move on to the integration tests to see what it look like to test more code at once.

Integration Tests

In my opinion, a good integration test won’t look very different from a good unit test. An integration test can still follow the AAA pattern like other automated tests. The parts that change are the tools you’ll use and the assertions you will write.

My definition of an integration test in Django is a test that uses Django’s test Client. In previous articles, I’ve only mentioned what a client is in passing. In the context of a web application, a client is anything that consumes the output of a web app to display it to a user.

The most obvious client for web app is a web browser, but there are plenty of other client types out there. Some examples that could use output from a web application:

  • A native mobile application
  • A command line interface
  • A programming library like Python’s requests package that can handle HTTP requests and responses

The Django test Client is like these other clients in that it can interact with your Django project to receive data from requests that it creates. The nice part about the test client is that the output is returned in a convenient way that we can assert against. The client returns the HttpResponse object directly!

With that context, here’s an integration test that we can discuss.

# application/tests/test_views.py

from django.test import Client
from django.urls import reverse

class TestProfileView:
    def test_shows_name(self):
        """The profile view shows the user's name."""
        client = Client()
        user = UserFactory()

        response = client.get(reverse("profile"))

        assert response.status_code == 200
        assert user.first_name in response.content.decode()

Enter fullscreen mode

Exit fullscreen mode

What is this test doing? Also, what is this test not doing?

By using the Django test client, the test runs a lot of Django code. This goes through:

  • URL routing
  • View execution (which will likely fetch from the database)
  • Template rendering

That’s a lot of code to execute in a single test! The goal of the test is to check that all the major pieces hang together.

Now let’s observe what the test is not doing. Even though the test runs a ton of code, there aren’t a huge number of assert statements. In other words, our goal with an integration isn’t to check every tiny little thing that could happen in the whole flow. Hopefully, we have unit tests that cover those little parts of the system.

When I write an integration test, I’m mostly trying to answer the question: does the system hold together without breaking?

Now that we’ve covered unit tests and integration tests, what are some tools that will help you make testing easier?

Tools To Help

When testing your application, you have access to so many packages to help that it can be fairly overwhelming. If you’re testing for the first time, you may be struggling with applying the AAA pattern and knowing what to test. We want to minimize the extra stuff that you have to know.

We’re going to revisit the tools that I listed earlier, pytest-django and factory_boy, to get you started. Consider these your Django testing survival kit. As you develop your testing skills, you can add more tools to your toolbox, but these two tools are a fantastic start.

pytest-django

pytest is a «test runner.» The tool’s job is to run automated tests. If you read Writing and running tests in the Django documentation, you’ll discover that Django also includes a test runner with ./manage.py test. What gives? Why am I suggesting that you use pytest?

I’m going to make a bold assertion: pytest is better. (Did I just go meta there? Yes, I did. 😆)

I like a lot about Django’s built-in test runner, but I keep coming back to pytest for one primary reason: I can use assert in tests. As you’ve seen in these test examples, the assert keyword makes for clear reading. We can use all of Python’s normal comparison tests (e.g., ==, !=, in) to check the output of tests.

Django’s test runner builds off the test tools that are included with Python in the unittest module. With those test tools, developers must make test classes that subclass unittest.TestCase. The downside of TestCase classes is that you must use a set of assert* methods to check your code.

The list of assert* methods are included in the unittest documentation. You can be very successful with these methods, but I think it requires remembering an API that includes a large number of methods. Consider this. Would you rather:

  1. Use assert? OR
  2. Use assertEqual, assertNotEqual, assertTrue, assertFalse, assertIs, assertIsNot, assertIsNone, assertIsNotNone, assertIn, assertNotIn, assertIsInstance, and assertNotIsInstance?

Using assert from pytest means that you get all the benefits of the assert* methods, but you only need to remember a single keyword. If that wasn’t enough, let’s compare the readability:

self.assertEqual(my_value, 42)
assert my_value == 42

self.assertNotEqual(my_value, 42)
assert my_value != 42

self.assertIsNotNone(my_value)
assert my_value is not None

self.assertTrue(my_value)
assert my_value

Enter fullscreen mode

Exit fullscreen mode

For the same reason that Python developers prefer property methods instead of getters and setters (e.g. obj.value = 42 instead of obj.set_value(42)), I think the assert style syntax is far simpler to visually process.

Outside of the awesome handling of assert, pytest-django includes a lot of other features that you might find interesting when writing automated tests.

factory_boy

The other test package that I think every developer should use in their Django projects is factory_boy.

factory_boy helps you build model data for your tests.

As you build up your Django project, you will have more models that help to describe the domain that your website addresses. Generating model data for your tests is a capability that is immensely valuable.

You could use your model manager’s create method to create a database entry for your test, but you’re going to run into some limits very fast.

The biggest challenge with using create comes from database constraints like foreign keys. What do you do if you want to build a record that requires a large number of non-nullable foreign key relationships? Your only choice is to create those foreign key records.

We can imagine an app that shows information about movies. The Movie model could have a variety of foreign key relationships like director, producer, studio, and so on. I’ll use a few in the example, but imagine what would happen as the number of foreign key relationships increases.

def test_detail_view_show_genre(client):
    """The genre is on the detail page."""
    director = Director.objects.create(name="Steven Spielberg")
    producer = Producer.objects.create(name="George Lucas")
    studio = Studio.objects.create(name='Paramount')
    movie = Movie.objects.create(
        genre='Sci-Fi', director=director, producer=producer, studio=studio
    )

    response = client.get(reverse('movie:detail', args=[movie.id]))

    assert response.status_code == 200
    assert 'Sci-Fi' in response.content.decode()

Enter fullscreen mode

Exit fullscreen mode

On the surface, the test isn’t too bad. I think that’s mostly because I kept the modeling simple. What if Director, Producer, or Studio also had required foreign keys? We’d spend most of our effort on the Arrangement section of the test. Also, as we inspect the test, we get bogged down with unnecessary details. Did we need to know the names of the director, producer, and studio? No, we didn’t need that for this test. Now, let’s look at the factory_boy equivalent.

def test_detail_view_show_genre(client):
    """The genre is on the detail page."""
    movie = MovieFactory(genre='Sci-Fi')

    response = client.get(reverse('movie:detail', args=[movie.id]))

    assert response.status_code == 200
    assert 'Sci-Fi' in response.content.decode()

Enter fullscreen mode

Exit fullscreen mode

MovieFactory seems like magic. Our test got to ignore all the other details. Now the test could focus entirely on the genre.

Factories simplify the construction of database records. Instead of wiring the models together in the test, we move that wiring to the factory definition. The benefit is that our tests can use the plain style that we see in the second example. If we need to add a new foreign key to the model, only the factory has to be updated, not all your other tests that use that model.

What might this Movie factory look like? The factory might be:

# application/tests/factories.py

import factory

from application.models import Movie

# Other factories defined here...

class MovieFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Movie

    director = factory.SubFactory(DirectorFactory)
    producer = factory.SubFactory(ProducerFactory)
    studio = factory.SubFactory(StudioFactory)
    genre = 'Action'

Enter fullscreen mode

Exit fullscreen mode

This factory definition is very declarative. We declare what we want, and factory_boy figures out how to put it together. This quality leads to factories that you can reason about because you can focus on the what and not the how of model construction.

The other noteworthy aspect is that the factories compose together. When we call MovieFactory(), factory_boy is missing data about everything so it must build all of that data. The challenge is that the MovieFactory doesn’t know how to build a Director or any of the movie’s foreign key relationships. Instead, the factory will delegate to other factories using the SubFactory attribute. By delegating to other factories, factory_boy can build the model and its entire tree of relationships with a single call.

When we want to override the behavior of some of the generated data, we pass in the extra argument as I did in the second example by providing «Sci-Fi» as the genre. You can pass in other model instances to your factories too.

factory_boy makes testing with database records a joy. In my experience, most of my Django tests require some amount of database data so I use factories very heavily. I think you will find that factory_boy is a worthy addition to your test tools.

Summary

In this article, we explored tests with Django projects. We focused on:

  • Why would anyone want to write automated tests
  • What kinds of tests are useful to a Django app
  • What tools can you use to make testing easier

Next time, we will dig into deployment. Deployment is getting your project into the environment where you will share your application for use. This might be the internet or it might be a private network for your company. Wherever you’re putting your app, you’ll want to know about:

  • Deploying your application with a Python web application server (i.e., ./manage.py runserver isn’t meant for deployed apps)
  • Deployment preconditions for managing settings, migrations, and static files
  • A checklist to confirm that your settings are configured with the proper security guards
  • Monitoring your application for errors

If you’d like to follow along with the series, please feel free to sign up for my newsletter where I announce all of my new content. If you have other questions, you can reach me online on Twitter where I am @mblayman.

Понравилась статья? Поделить с друзьями:
  • Django admin ошибка проверки csrf запрос отклонен
  • Django admin server error 500
  • Django admin error messages
  • Django admin csrf error
  • Django 500 error ajax