Documentation Index
Fetch the complete documentation index at: https://mintlify.com/opengisch/qfieldcloud/llms.txt
Use this file to discover all available pages before exploring further.
QFieldCloud has a comprehensive test suite covering unit tests, integration tests, and API tests.
Test Framework
QFieldCloud uses Django’s built-in test framework with a custom test runner:
- Test Runner:
QfcTestSuiteRunner (see docker-app/qfieldcloud/testing.py)
- Database: Separate test database created automatically
- Isolation: Each test runs in a transaction that’s rolled back
Setup Test Environment
Add the test override file to your COMPOSE_FILE in .env:
export COMPOSE_FILE=docker-compose.yml:docker-compose.override.standalone.yml:docker-compose.override.test.yml
Rebuild Stack
Rebuild to install test dependencies from requirements_test.txt:
docker compose up -d --build
docker compose run app python manage.py migrate
docker compose run app python manage.py collectstatic --noinput
Running Tests
Run All Tests
docker compose run app python manage.py test --keepdb
The --keepdb flag preserves the test database between runs for faster execution.
Run Specific Test Module
Test a specific module (e.g., permissions):
docker compose run app python manage.py test --keepdb qfieldcloud.core.tests.test_permission
Run Specific Test Case
Test a specific test class:
docker compose run app python manage.py test --keepdb qfieldcloud.core.tests.test_permission.QfcTestCase
Run Specific Test Method
Test a single test method:
docker compose run app python manage.py test --keepdb qfieldcloud.core.tests.test_permission.QfcTestCase.test_collaborator_project_takeover
Test Structure
Tests are organized by Django app:
docker-app/qfieldcloud/
├── core/tests/
│ ├── test_api.py # API endpoint tests
│ ├── test_permission.py # Permission tests
│ ├── test_project.py # Project model tests
│ ├── test_jobs.py # Job processing tests
│ ├── test_delta.py # Delta sync tests
│ └── ...
├── authentication/tests/
│ ├── test_authentication.py # Auth backend tests
│ └── test_list_auth_providers.py
├── filestorage/tests/
│ ├── test_files_api.py # File API tests
│ ├── test_storage_usage.py # Storage quota tests
│ ├── test_webdav.py # WebDAV backend tests
│ └── ...
├── subscription/tests/
│ ├── test_subscription.py # Subscription tests
│ ├── test_package.py # Package tests
│ └── ...
└── notifs/tests/
└── test_notifs.py # Notification tests
Writing Tests
Basic Test Example
from django.test import TestCase
from qfieldcloud.core.models import User, Project
class ProjectTestCase(TestCase):
def setUp(self):
"""Create test fixtures"""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.project = Project.objects.create(
name='Test Project',
owner=self.user
)
def test_project_creation(self):
"""Test that a project can be created"""
self.assertEqual(self.project.name, 'Test Project')
self.assertEqual(self.project.owner, self.user)
def test_project_str(self):
"""Test project string representation"""
expected = f'{self.user.username}/{self.project.name}'
self.assertEqual(str(self.project), expected)
API Test Example
from rest_framework.test import APITestCase
from rest_framework import status
from qfieldcloud.core.models import User
class ProjectAPITestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
def test_create_project(self):
"""Test creating a project via API"""
url = '/api/v1/projects/'
data = {
'name': 'My Test Project',
'description': 'A test project'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], 'My Test Project')
def test_list_projects_requires_auth(self):
"""Test that listing projects requires authentication"""
self.client.force_authenticate(user=None)
response = self.client.get('/api/v1/projects/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Testing with Files
from django.core.files.uploadedfile import SimpleUploadedFile
from rest_framework.test import APITestCase
class FileUploadTestCase(APITestCase):
def test_upload_qgis_file(self):
# Create a mock QGIS project file
qgs_content = b'<?xml version="1.0" encoding="utf-8"?>'
qgs_file = SimpleUploadedFile(
"project.qgs",
qgs_content,
content_type="application/x-qgis-project"
)
# Upload via API
url = f'/api/v1/files/{self.project.id}/project.qgs'
response = self.client.post(url, {'file': qgs_file})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Testing Permissions
from qfieldcloud.core.models import Project, ProjectCollaborator
from qfieldcloud.core.models import ProjectCollaborator as PC
class PermissionTestCase(APITestCase):
def test_editor_cannot_delete_project(self):
"""Test that an editor cannot delete a project"""
# Create owner and editor
owner = User.objects.create_user(username='owner')
editor = User.objects.create_user(username='editor')
# Create project
project = Project.objects.create(name='Test', owner=owner)
# Add editor as collaborator
ProjectCollaborator.objects.create(
project=project,
collaborator=editor,
role=PC.Roles.EDITOR
)
# Try to delete as editor
self.client.force_authenticate(user=editor)
response = self.client.delete(f'/api/v1/projects/{project.id}/')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Test Coverage
Generate Coverage Report
Run tests with coverage:
docker compose exec app coverage run manage.py test --keepdb
View Coverage Report
Display coverage in terminal:
docker compose exec app coverage report
Example output:
Name Stmts Miss Cover
-------------------------------------------------------------
qfieldcloud/core/models.py 450 12 97%
qfieldcloud/core/views.py 380 25 93%
qfieldcloud/authentication/backends.py 85 8 91%
-------------------------------------------------------------
TOTAL 3421 156 95%
Generate HTML Coverage Report
docker compose exec app coverage html
This creates an HTML report in htmlcov/. You can view it by opening htmlcov/index.html in a browser.
Coverage Configuration
Coverage settings are in docker-app/.coveragerc.
Parallel Test Instance
You can run a test instance in parallel with your development instance.
Create .env.test
ENVIRONMENT=test
QFIELDCLOUD_HOST=nginx
DJANGO_SETTINGS_MODULE=qfieldcloud.settings
STORAGE_ENDPOINT_URL=http://172.17.0.1:8109
MINIO_API_PORT=8109
MINIO_BROWSER_PORT=8110
WEB_HTTP_PORT=8101
WEB_HTTPS_PORT=8102
HOST_POSTGRES_PORT=8103
QFIELDCLOUD_DEFAULT_NETWORK=qfieldcloud_test_default
QFIELDCLOUD_SUBSCRIPTION_MODEL=subscription.Subscription
DJANGO_DEV_PORT=8111
SMTP4DEV_WEB_PORT=8112
SMTP4DEV_SMTP_PORT=8125
SMTP4DEV_IMAP_PORT=8143
COMPOSE_PROJECT_NAME=qfieldcloud_test
COMPOSE_FILE=docker-compose.yml:docker-compose.override.standalone.yml:docker-compose.override.test.yml
DEBUG_APP_DEBUGPY_PORT=5781
DEBUG_WORKER_WRAPPER_DEBUGPY_PORT=5780
Build Test Stack
docker compose --env-file .env --env-file .env.test up -d --build
docker compose --env-file .env --env-file .env.test run app python manage.py migrate
docker compose --env-file .env --env-file .env.test run app python manage.py collectstatic --noinput
Run Tests on Test Stack
docker compose --env-file .env --env-file .env.test run app python manage.py test --keepdb
Test Database
Test Database Configuration
The test database is automatically created with the name defined in POSTGRES_DB_TEST environment variable.
In settings.py:218-221:
"TEST": {
"NAME": os.environ.get("POSTGRES_DB_TEST"),
}
Access Test Database
Update your ~/.pg_service.conf:
[test.localhost.qfield.cloud]
host=localhost
dbname=test_qfieldcloud_db
user=qfieldcloud_db_admin
port=5433
password=3shJDd2r7Twwkehb
sslmode=disable
Connect:
psql 'service=test.localhost.qfield.cloud'
Best Practices
1. Use setUp() and tearDown()
Create fixtures in setUp(), clean up in tearDown():
def setUp(self):
self.user = User.objects.create_user(username='test')
def tearDown(self):
# Usually not needed - Django handles cleanup
pass
2. Use --keepdb for Speed
Always use --keepdb to avoid recreating the test database:
python manage.py test --keepdb
3. Test One Thing at a Time
Each test method should test one specific behavior:
def test_user_can_create_project(self):
# Test only project creation
def test_user_can_update_project_name(self):
# Test only name update
4. Use Descriptive Test Names
Test names should describe what they test:
def test_editor_cannot_delete_files(self): # Good
def test_permissions(self): # Too vague
5. Use Assertions Effectively
self.assertEqual(actual, expected)
self.assertTrue(condition)
self.assertFalse(condition)
self.assertRaises(Exception, callable)
self.assertIn(item, container)
self.assertIsNone(value)
self.assertIsNotNone(value)
6. Mock External Services
Use unittest.mock for external dependencies:
from unittest.mock import patch, MagicMock
@patch('qfieldcloud.core.tasks.send_email')
def test_notification_sent(self, mock_send_email):
# Test logic
mock_send_email.assert_called_once()
Continuous Integration
QFieldCloud tests are run on every pull request. Ensure:
- All tests pass locally before pushing
- New features include tests
- Test coverage doesn’t decrease
- Tests run in reasonable time
Next Steps