Testing API
Last Updated: December 2022
Manually testing your app can be very time consuming, especially when modifying a simple line of code usually warrants retesting everything. To help automate and streamline the testing of your app, Tethys Platform provides you with a great starting point by providing the following:
A
tests
directory with atests.py
script within your app's default scaffold that contains well-commented sample testing code.The Testing API which provides a helpful test class for setting up your app's testing environment.
Writing Tests
Tests should be written in a separate python script that is contained somewhere within your app's scaffold. By default, a tests
directory already exists in the app-level directory and contains a tests.py
script. Unless you have a good reason not to, it would be best to start writing your test code here.
As an example, if wanting to automate the testing of a the map controller in the "My First App" from the tutorials, the tests.py
script might be modified to look like the following:
from tethys_sdk.testing import TethysTestCase
from ..app import MyFirstApp
class MapControllerTestCase(TethysTestCase):
def set_up(self):
self.create_test_persistent_stores_for_app(MyFirstApp)
self.create_test_user(username="joe", email="joe@some_site.com", password="secret")
self.client = self.get_test_client()
def tear_down(self):
self.destroy_test_persistent_stores_for_app(MyFirstApp)
def test_success_and_context(self):
self.client.force_login(self.user)
response = self.client.get('/apps/my-first-app/map/')
# Check that the response returned successfully
self.assertEqual(response.status_code, 200)
# Check that the response returned the context variable
self.assertIsNotNone(response.context['map_options'])
Tethys Platform leverages the native Django testing framework (which leverages the unittests Python module) to make writing tests for your app much simpler. While Tethys Platform encapsulates most of what is needed in its Testing API, it may still be necessary to refer to the Django and Python documentation for additional help while writing tests. Refer to their documentation here:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#writing-tests
https://docs.python.org/2.7/library/unittest.html#module-unittest
Testing Controllers that Use OAuth2 Authentication
Important
This feature requires the social-auth-app-django
library to be installed. Starting with Tethys 5.0 or if you are using micro-tethys-platform
, you will need to install social-auth-app-django
using conda or pip as follows:
# conda: conda-forge channel strongly recommended
conda install -c conda-forge social-auth-app-django
# pip
pip install social-auth-app-django
Using the force_login
method above works great for testing controllers where login is required. However, additional steps are required to test controllers that must be authenticated with a specific OAuth2 provider (i.e. specify the ensure_oauth_provider
argument to the controller
decorator). For example, if you have a controller like this:
@controller(
ensure_oauth2_provider="google-oauth2"
)
def home(request):
...
Then your test will need to define the UserSocialAuth
database object to register the test user with the oauth provider. This can be done in the setUpClass
method of your test class:
from unittest import mock
from social_django.models import UserSocialAuth
from tethys_sdk.testing import TethysTestCase
class MapControllerTestCase(TethysTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.client = cls.get_test_client()
cls.user = cls.create_test_user(username="joe", password="secret", email="joe@some_site.com")
UserSocialAuth(user=cls.user, provider="google-oauth2").save()
Once the UserSocialAuth
object is defined you can then use the force_login
method as before:
def test_success_and_context(self):
self.client.force_login(self.user)
response = self.client.get('/apps/my-first-app/map/')
# Check that the response returned successfully
self.assertEqual(response.status_code, 200)
# Check that the response returned the context variable
self.assertIsNotNone(response.context['map_options'])
Running Tests
To run tests for an app:
Open a terminal and activate the Tethys environment:
conda activate tethys
In portal_config.yml make sure that the default database user is set to
tethys_super
or is a super user of the database:
DATABASES: default: ENGINE: django.db.backends.postgresql_psycopg2 NAME: tethys_platform USER: tethys_super PASSWORD: pass HOST: 127.0.0.1 PORT: 5435
From the root directory of your app, run the
tethys manage test
command:
tethys manage test tethysapp/<app_name>/tests
This will run all tests defined in the
tests
directory of your app. If you would like to run just a subset of tests then you can specify which tests to run with the following:To run all tests in a specific module within the
tests
directory:tethys manage test tethysapp/<app_name>/tests/<module_name> # or tethys manage test tethysapp.<app_name>.tests.<module_name>
To run all tests within specific class of a test module:
tethys manage test tethysapp.<app_name>.tests.<module_name>.<class_name>
And to run a single test method within a test class:
tethys manage test tethysapp.<app_name>.tests.<module_name>.<class_name>.<method_name>
For example, to run just the
test_success_and_context
test method defined above you would use the following command:tethys manage tests tethysapp.my_first_app.tests.tests.MapControllerTestCase.test_success_and_context
Important
When specifying the tests to run using a system path (e.g. tethysapp/<app_name>/tests/
) you must provide either an absolute path or the path relative to your current working directory.
When specifying the tests to run using a Python module (e.g. tethysapp.<app_name>.tests.<module_name>
) then your current working directory is irrelevant, but note that this format will only work for modules under the tests
directory. You cannot specify the whole tests
directory as a Python module.
API Documentation
- class tethys_apps.base.testing.testing.TethysTestCase(methodName='runTest')
This class inherits from the Django TestCase class and is itself the class that is should be inherited from when creating test case classes within your app. Note that every specific test written within your custom class inheriting from this class must begin with the word "test" or it will not be executed during testing.
- static create_test_persistent_stores_for_app(app_class)
Creates temporary persistent store databases for this app to be used in testing.
- Parameters:
app_class -- The app class from the app's app.py module
- Returns:
None
- static create_test_superuser(username, password, email=None)
Creates and returns a temporary superuser to be used in testing
- Parameters:
username (string) -- The username for the temporary test user
password (string) -- The password for the temporary test user
email (string) -- The email address for the temporary test user
- Returns:
User object
- static create_test_user(username, password, email=None)
Creates and returns temporary user to be used in testing
- Parameters:
username (string) -- The username for the temporary test user
password (string) -- The password for the temporary test user
email (string) -- The email address for the temporary test user
- Returns:
User object
- static destroy_test_persistent_stores_for_app(app_class)
Destroys the temporary persistent store databases for this app that were used in testing.
- Parameters:
app_class -- The app class from the app's app.py module
- Returns:
None
- static get_test_client()
Returns a Client object to be used to mimic a browser in testing
- Returns:
Client object
- set_up()
This method is to be overridden by the custom test case classes that inherit from the TethysTestCase class and is used to perform any set up that is applicable to every test function that is defined within the custom test class
- Returns:
None
- tear_down()
This method is to be overridden by the custom test case classes that inherit from the TethysTestCase class and is used to perform any tear down that is applicable to every test function that is defined within the custom test class. It is often used in conjunction with the "set_up" function to tear down what was setup therein.
- Returns:
None