© Ben Lopatin 2020
B. LopatinDjango Standalone Appshttps://doi.org/10.1007/978-1-4842-5632-9_3

3. Testing

Ben Lopatin1 
(1)
New York, NY, USA
 

Tests ensure that our code does what we expect it to do. They also ensure that changes to the codebase don’t break something unexpected, and they signal to other users of our app that they can depend on the code.

In this chapter, you’ll learn exactly how tests provide value in a standalone Django app, how to run your app’s tests outside of a Django project, how to test more complicated multi-app relationships, how to include your tests, and also whether or not you need to configure Django in order to test your app.

Why test?

Everyone says that you should test. It sounds obvious - if testing is good, we should do it. But this begs the question about the benefits of testing.

Testing your Django app several purposes. Written in conjunction with, or before your application code, tests helps provide a working specification against which your code can be verified. In this capacity they can also help shape the code and interface, as if you’re adding some feature from scratch, a test will give you your first chance of using it.

Once in place, even otherwise trivial tests serve to protect against regressions introduced by seemingly trivial changes to the codebase.

While not their primary use, tests can also provide an example of how to use your code. In this capacity they’re certainly not a replacement for proper documentation, but tests as code examples - especially when tests are run automatically - are a form of documentation that you can verify is up to date.

Underlying all of this is the fact that computer programs are written by human beings and we humans are terribly unreliable when it comes to writing reliable code on our own (apologies if this does not apply to you). There are all kinds of things we can’t predict, edge cases we’re not good at seeing right away, and interactions that aren’t obvious at the surface of our code.

Testing doesn’t solve all of these problems, but tests provide a potent tool to remove a lot of uncertainty about our code. Ultimately tests provide confidence, both for you and other users of your app - and don’t forget that “future you” is likely one of those users!

Testing apps from a Django project

Django projects provide a way to run tests with the test management command:
python manage.py test
This command will run all tests in your Django project. The scope can be narrowed to run only individually namedi apps by using the test management command combined with the app name, like so:
python manage.py test myapp
So if the very simple myapp looks like this
myapp/
    __init__.py
    models.py
    tests.py
with a simple tests.py file like so
from django.test import TestCase
from myapp.models import SomeModel
class TestSomeModel(TestCase):
        def test_str_method(self):
                instance = SomeModel()
                self.assertEqual(f"{instance}", "<Unnamed Instance>")

then the command python manage.py test myapp will run all of the test in myapp.tests using Django’s default test runner, for example, with the example tests file given, the command will run the TestSomeModel.test_str_method.

This works just fine when you’re working from a larger Django project, for example, if you’re developing your app in the context of a working project. It’s of much less help if your app is a standalone library where the code is intended to be managed from outside of a project. For a standalone app, it’d be much preferable to be able to run tests just like any other Python package.

Testing the app

If you’ve worked with other Python packages before, you’ll have noticed that they’re tested in a straightforward way. There’ll be a test module somewhere and usually the setup.py file defines a test script to run using the python setup.py test command. That works for packages using Django, too, with the caveat that much Django functionality must be run from the context of a Django project, something Python’s unittest won’t take care of for you.

To motivate some reasonable ways of testing a standalone app, let’s consider the most immediately available strategy for testing the app: testing from whatever project you’re using the app in (presuming you are extracting it).

This means that to test the myapp app, it needs to be installed on the same path as your working project, that is, the same virtual environment, and that it needs to be in your working project’s INSTALLED_APPS. When it’s time to test changes to myapp, you’ll need to go back to the working project to run them, that is, running ./manage.py test myapp.

If this sounds less than sensible, you’re on the right track. However, this strategy doesn’t allow testing a standalone app, which means it’s not repeatable for anyone else who isn’t working with your project. And if you’re going to package the app for reuse, you won’t have recourse to your original project. Thankfully there’s a better way.

Testing outside of a project

To motivate our subsequent solutions, we’ll set up the most obvious solution possible. This will entail setting up a dummy, or holder, project and running the tests from there. To do, we would create new Django project in our app’s root folder, parallel to the app source folder itself. This project will then include our app in the INSTALLED_APPS list.

Then, running the tests and any other commands is as simple as invoking the holder project’s manage.py file just like any other project.

Next step is to create an example project in the package root that will be a stripped down project only including our app. Now we can run manage.py commands directly in our package and test the app. You might even add a bash script at the project root that will execute the tests no matter where they’re located.
#!/bin/bash
cd sample_project
python manage.py test myapp
Here’s what the layout would look like:
sample_project
    __init__.py
    settings.py
    url.spy
    wsgi.py
  __init__.py
  manage.py
myapp/
    __init__.py
    models.py
    tests.py
Then to run the tests for your app, you’d run them from the example project just as if it were a production-ready Django project:
python manage.py test myapp

This works and is an improvement over the original example, but it still adds more than necessary just to run our tests.

Using a testing script

Of course, Django doesn’t demand that we have project scaffolding, just that Django settings are configured. So a better solution is a Python script that configures those minimalist settings and then runs the tests.

The script needs to do three things:
  1. 1.

    Define or configure Django settings

     
  2. 2.

    Trigger Django initialization (i.e., with django.setup())

     
  3. 3.

    Execute the test runner

     
There are two ways to provide Django settings. One is to configure them directly in a test script with keyword arguments for settings.configure(). The other is to point to a test-only settings.py module, just as you would, running a production app. The following is a small example of the former:
#!/usr/bin/env python
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
    settings.configure(
        DATABASES={"default": {
            "ENGINE": "django.db.backends.sqlite3"
        }},
        ROOT_URLCONF="tests.urls",
        INSTALLED_APPS=[
            "django.contrib.auth",
            "django.contrib.contenttypes",
            "myapp",
        ],
    )  # Minimal Django settings required for our tests
        django.setup()  # configures Django
        TestRunner = get_runner(settings)  # Gets the test runner class
        test_runner = TestRunner()  # Creates an instance of the test runner
        failures = test_runner.run_tests(["tests"])  # Run tests and gather failures
        sys.exit(bool(failures))  # Exits script with error code 1 if any failures
And using a settings module instead (from the following Django documentation). This is functionally the same as the preceding code except it breaks out the settings into a more typical settings file, in this case tests/test_settings.py
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
    os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
    django.setup()
    TestRunner = get_runner(settings)
    test_runner = TestRunner()
    failures = test_runner.run_tests(["tests"])
    sys.exit(bool(failures))

Why choose one over the other? Using a separate settings module will be more flexible if you have other needs for the settings. The in-script configuration style suffices for simpler apps, for example, those without models.

In Chapter 22 we’ll examine a more ergonomic way of managing your tests and test configuration.

Testing application relationships

What happens though when your Django app is designed to be used with other apps, or used in conjunction with them? Testing only your app in isolation is not enough. In this case you’ll need to create sample apps and include them in your test settings.

Let’s say your app provides base models. For our example it’s a very basic ecommerce module that lets people make a product out of any model they want, adding some basic fields like price, a SKU, and whether it’s actively sold or not. The app also includes a queryset class with some helpful methods defined. Since our model class is abstract, the queryset class has to be associated with a concrete model in the user’s app.
class ProductsQuerySet(models.QuerySet):
        def in_stock(self):
                return self.filter(is_in_stock=True)
class ProductBase(models.Model):
    sku = models.CharField()
    price = models.DecimalField()
    is_in_stock = models.BooleanField()
    class Meta:
        abstract = True

Now to test this, we need a concrete model (and it would be helpful to have tests actually using the base model anyhow). To do this we’ll need another app that defines a concrete model inheriting from our abstract model, and that uses the provided queryset.

Such an app need only provide the bare minimum to be an app, specifically the models.py module:
test_app/
        migrations/ ...
    __init__.py
    models.py
And in your models file, define a model using your app’s abstract base model:
from myapp.models import ProductBase, ProductQuerySet
class Pen(ProductBase):
    """Testing app model"""
    name = models.CharField()
    pen_type = models.CharField()
        objects = ProductQuerySet.as_manager()
With the models defined, make sure the test app is included in your test settings INSTALLED_APPS:
INSTALLED_APPS = [
    'myapp',
    'test_app',
]

Note that this applies to Django packages that are not installable apps as well if they require any level of integration testing.

Where to include tests

When you add tests to apps inside of your Django projects, you probably include test modules inside each app, either with a single file or a directory:
myapp/
    __init__.py
    models.py
        tests.py
This will work for standalone apps too, but generally should be avoided. Your tests in this case should live in a separate, top-level module outside of your app. If you’re testing with additional modules, like test apps, then this ensures that there are no dependencies on non-installed modules within the code that ships with your app. It also keeps the installed package cleaner (although it’s worth noting that this is not a unanimous opinion).
myapp/
    __init__.py
    models.py
test_app/
    __init__.py
    models.py
tests/
    __init__.py
    test_models.py

Testing without Django

The emphasis here is on Django apps, that is, Python modules that can be installed and included in a Django project to use models, template tags, management commands, among others. But in many cases the functionality provided by apps can be tested as plain old Python code.

This will be the case with anything in your app that requires setup, like models. However, this isn’t true of every part of Django or every part of your app. And in fact if your app doesn’t have any models, and you don’t have any request-related functionality to test - especially at an integration test level - then you can forgo with setting up or using Django’s test modules, sticking to the standard library’s unittest, or any other testing framework you so choose.

You will only need to invoke a test runner through Django if you’re loading the Django project, for example, anything involving models, settings, or a full request/response cycle. In most cases, testing features like forms, the logic in template tags and filters and others, is not dependent on any of the parts of Django that require project setup.

Why would you do this? It’s extraordinarily doubtful that the performance gains from using unittest over django.test are going to be noticeable to say nothing of impactful. However, if these are the only tests that you need, then your testing environment will be simpler to set up and run.

Summary

In this chapter, you learned why it’s important to have tests for your standalone app and how to begin testing a Django app when it’s no longer part of a parent Django project. You also learned how to simplify test execution with a Python script that handles Django setup and how to test app features that are predicated on relationships defined by other apps outside of your own. Lastly, you learned where to include the tests for you standalone app, in a top-level tests directory, and that, for some types of apps that don’t rely on the database or template engine, it may be sufficient to use Python’s unittest library without the Django setup.

In the next chapter, you’ll learn how to manage database migrations for your app without a Django project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.1.239