Skip to content

Writing Tests

Python tests (pytest)

Test structure

tests/
├── conftest.py ← shared fixtures
└── orchid/
├── migrations/ ← migration-specific tests
├── models/ ← model tests
└── utils/ ← utility function tests
├── test_email_preview.py
├── test_feature_flags.py
├── test_forms.py
├── test_html_sanitizer.py
└── test_recaptcha.py

Available fixtures (from tests/conftest.py)

@pytest.fixture
def app():
"""Flask app with SQLite in-memory DB, CSRF disabled."""
# Creates all tables, yields app, drops all tables
@pytest.fixture
def client(app):
"""Test client — use for HTTP request testing."""
@pytest.fixture
def runner(app):
"""CLI runner — use for testing Flask CLI commands."""
@pytest.fixture
def app_context(app):
"""Application context — use when you need DB access without HTTP."""

Writing a unit test

tests/orchid/utils/test_my_utility.py
import pytest
from orchid.utils.my_utility import my_function
class TestMyFunction:
def test_basic_case(self):
result = my_function('input')
assert result == 'expected output'
def test_edge_case_empty_string(self):
result = my_function('')
assert result is None
def test_raises_on_invalid_input(self):
with pytest.raises(ValueError):
my_function(None)

Writing a model test

tests/orchid/models/test_my_model.py
import pytest
from orchid import models
class TestMyModel:
def test_create_record(self, app_context):
from orchid.utils.flaskutils import Crudler
record = models.MyModel(
name='Test',
case_id=None # or create a case first
)
Crudler.add(record)
Crudler.commit()
fetched = models.MyModel.query.filter_by(name='Test').first()
assert fetched is not None
assert fetched.name == 'Test'

Writing an integration test (HTTP)

tests/orchid/test_my_view.py
import pytest
class TestMyView:
def test_unauthenticated_redirects(self, client):
response = client.get('/admin/my-page')
assert response.status_code in (302, 401, 403)
def test_authenticated_returns_200(self, client, app_context):
# Log in first
with client.session_transaction() as sess:
sess['logged_in'] = True
sess['user_type'] = 'admin'
sess['admin_id'] = 1 # use a seeded admin ID
response = client.get('/admin/my-page')
assert response.status_code == 200

Running tests

Terminal window
pytest # all tests
pytest tests/orchid/utils/ # all utils tests
pytest tests/orchid/utils/test_forms.py # single file
pytest -m unit # unit tests only
pytest -m integration # integration tests only
pytest -v # verbose output
pytest --tb=short # shorter tracebacks

Test markers

Use markers to categorize tests:

import pytest
@pytest.mark.unit
def test_something():
pass
@pytest.mark.integration
def test_something_else(app_context):
pass

Markers are registered in pytest.ini. Run specific markers with -m unit or -m integration.

JavaScript tests (Jest)

Test structure

tests/js/
├── setup.js ← Jest setup file (jsdom config)
└── **/*.test.js ← test files (mirror static/js structure)

Writing a Jest test

tests/js/utils/my_utility.test.js
import { myFunction } from '../../../static/js/utils/my_utility.mjs';
describe('myFunction', () => {
test('returns expected output for basic input', () => {
expect(myFunction('input')).toBe('expected output');
});
test('handles empty string', () => {
expect(myFunction('')).toBeNull();
});
test('throws on null input', () => {
expect(() => myFunction(null)).toThrow();
});
});

Running JS tests

Terminal window
npm test # run all
npm run test:watch # watch mode
npm run test:coverage # with coverage report

Coverage

JS coverage is collected from static/js/**/*.{js,mjs} (excluding .min.js and vendor/). Run npm run test:coverage to see which modules have low coverage.

What to test (prioritization)

Given that the test suite is thin, prioritize in this order:

  1. Business logic utility functions — anything in orchid/utils/ that has no side effects is easy to unit test and high value
  2. Jinja filters — test that filters produce the right output for edge cases (null inputs, unexpected formats)
  3. Model validation — test that models enforce the constraints you care about
  4. API endpoints — integration tests that verify the right HTTP status codes and response shapes
  5. View endpoints — lower priority (mostly just check they return 200 and don’t crash)