User:Eduralph/Sandbox/Gramps 6.0 Wiki Manual - Addon Development - Testing
← Previous · Index · Next →
Contents
Overview
How to test an addon without launching the GUI on every iteration — the test framework, the layout conventions, the fixtures that work, and the platform-aware rules that keep tests portable across Linux, Windows, and Mac.
A working test suite is what makes an addon maintainable across Gramps releases. The matrix of (Gramps version × OS) makes manual testing impossible at scale; the per-OS prefix conventions below let a single CI matrix verify your addon against every supported combination automatically.
Framework: stdlib unittest
Use stdlib unittest. Don't use pytest.
Gramps itself standardises on unittest (subclasses of unittest.TestCase), which keeps addon tests contributable upstream without a framework-conversion step. Mixing pytest features (fixtures, parametrise, plugins) breaks contribution upstream where pytest isn't installed.
import unittest
class MyAddonTests(unittest.TestCase):
def test_handles_empty_input(self):
# ...
self.assertEqual(result, expected)
if __name__ == "__main__":
unittest.main()
Class header convention
The "class header navigation comment" rule from gramps' AGENTS.md is unconditional — it applies to unittest.TestCase subclasses too. PR 2326 round 2 caught the omission:
# ------------------------------------------------------------
#
# MyAddonTests
#
# ------------------------------------------------------------
class MyAddonTests(unittest.TestCase):
...
Layout
Each addon ships its tests in a tests/ subpackage:
MyAddon/
├── MyAddon.gpr.py
├── MyAddon.py
└── tests/
├── __init__.py # marker — see below
└── test_myaddon.py
Why tests/__init__.py exists
The marker is hygiene, not a bug fix. Python 3.3+'s implicit namespace packages (PEP 420) mean a directory without __init__.py is still importable; dotted-path loading (python3 -m unittest MyAddon.tests.test_myaddon) works either way. But:
- Explicit beats implicit. "It works" is currently true by accident of invocation. The same code breaks the moment something uses
discoveror assumes regular packages. - It's the prerequisite for centralisation. Shared per-addon test setup — fixtures, the GTK-pin contract, presence meta-tests — has no home until
tests/__init__.pyexists. Empty marker now, contract-bearing later.
The convention crystallises as: every addon's tests/ should have an __init__.py; the addon directory itself should not.
The asymmetry matters. The addon directory must remain a plain namespace dir — Gramps' plugin loader puts the addon dir on sys.path and imports <Addon>.py by name. Making the addon dir a regular package can disturb plugin loading (and the Mantis 12691 namespace trap lives in exactly this area). The tests/ subfolder has no such constraint, so making it an explicit package is free.
This is what addons-source PR 930 (Gary Griffin) is moving toward.
Filename conventions (addons-source CI)
addons-source's CI workflow filters tests by filename prefix to scope them per platform:
| Prefix | Where it runs |
|---|---|
test_*.py
|
All platforms (Linux + Windows) |
test_linux_*.py
|
Linux only |
test_windows_*.py
|
Windows only |
test_integration_*.py
|
Linux only — full-pipeline / DB-backed |
The Ubuntu runner skips test_windows_*; the Windows runner skips both test_linux_* and test_integration_*. Both runners include the platform-neutral test_*.py files.
Pick the prefix that matches the test's portability, not the platform you happen to be developing on. A test that exercises POSIX file paths goes under test_linux_*; a test that exercises win32 locale handling goes under test_windows_*; everything else, the plain test_*.py prefix.
CI's workflow file is authoritative: addons-source/.github/workflows/ci.yml.
Loading: dotted path, not discover
Upstream CI loads tests by dotted path:
python3 -m unittest MyAddon.tests.test_myaddon
Not by discover from a tests/ directory. The reason: dotted-path loading surfaces the namespace-package trap. Bug 12691 — from <Addon> import <Addon> binding the submodule instead of the class — only shows up under dotted-path loading. discover-based loading walks files by filename, hiding the import-resolution issue. Mirroring CI's invocation locally catches what CI catches.
Locally, from the addons-source root, the same invocation works:
# Run one test module python3 -m unittest MyAddon.tests.test_myaddon # Run every test in the addon's tests/ package python3 -m unittest discover -s MyAddon/tests -t .
The discover form here works because the addon directory is the import root — the namespace-package trap shows up only when an individual addon module mis-imports itself.
Mocked vs example.gramps-backed tests
Two complementary strategies. They're not alternatives.
Mocked unit tests
Fast, no DB on disk, suitable for tight branch-coverage of pure logic. Substitute the database with a stub that returns fixed objects:
import unittest
from unittest.mock import MagicMock
class HappyPathTests(unittest.TestCase):
def test_skips_people_without_birth(self):
person = MagicMock()
person.get_birth_ref.return_value = None
result = pure_logic(person)
self.assertEqual(result, expected)
The MagicMock approach has a built-in failure mode: it returns something for every method call, so a typo'd method name appears to work. Real DB code that fails on the next call will pass the mocked test. This is the bug the next strategy catches.
example.gramps-backed tests
example.gramps ships with the Gramps source under example/gramps/example.gramps. It's the canonical fixture triage and developers reproduce against; loading it produces a real populated database with the cross-typed backlinks, ID normalisations, and absent optional fields that real users hit.
import os
import unittest
from gramps.gen.db.utils import open_database
class IntegrationTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db = open_database(
os.path.expanduser("~/path/to/gramps/example/gramps/example.gramps")
)
def test_handles_real_data(self):
result = code_under_test(self.db)
self.assertGreater(len(result), 0)
Running tests locally
From the addons-source checkout root:
# Run one addon's tests python3 -m unittest discover -s MyAddon/tests -t . # Or invoke a single test module by dotted path (mirrors CI's invocation) python3 -m unittest MyAddon.tests.test_myaddon
The Python that runs the tests needs gramps importable. The simplest setup is PYTHONPATH=/path/to/gramps python3 -m unittest …; if Gramps is installed system-wide, the import resolves without PYTHONPATH.
On Windows, run from the MSYS2 UCRT64 shell against a UCRT64-installed Gramps — the AIO build for Gramps 6.1+ targets UCRT64; Gramps 6.0 isn't Windows-tested upstream. See 13-compatibility → Windows toolchain migrated to UCRT64L-compatibility.md#windows-toolchain-migrated-to-ucrt64).
## See also
- [05-fundamentals → Logging — LOG setup that tests assert against.
- 06-data-access → Testing data access — DB-API patterns to exercise.
- 09-debug — turning a repro script into a test.
- 10-troubleshoot — the symptoms these tests catch in CI rather than production.
- 11-code-analysis — what the static checkers verify before tests run.
- [16-guidelines → TestingN-guidelines.md#testing) — normative rules.
- Mantis 12691 — the canonical namespace-package trap that motivates dotted-path loading.
- addons-source PR 930 —
tests/__init__.pyconvention.
|
This article's content is incomplete or a placeholder stub. |