Writing Your First Test
A worked walkthrough of everything in nvda-addon-testkit, using
examples/demo-addon/ — the same add-on this kit tests itself against — as
the add-on under test. By the end you will have run one real end-to-end test
against a real NVDA.
Windows only: NVDA cannot run headless on Linux or macOS. See Installation for CI, where the runner provides Windows for you.
1. Install and configure
pip install nvda-addon-testkit
Point the plugin at your add-on’s built bundle in pyproject.toml:
[tool.nvda-testkit]
addon-bundle = "examples/demo-addon.nvda-addon"
addon-bundle is a glob, not a literal path — it can match a version number
your build script stamps into the filename. The addon_bundle fixture fails
loudly if the pattern matches zero or more than one file, rather than
guessing.
2. Build the bundle
The demo add-on builds with a small zip script:
#!/usr/bin/env python3
"""Zip the demo add-on into examples/demo-addon.nvda-addon."""
from __future__ import annotations
import zipfile
from pathlib import Path
SOURCE = Path(__file__).resolve().parent
BUNDLE = SOURCE.parent / "demo-addon.nvda-addon"
def build() -> Path:
files = sorted(
path
for path in SOURCE.rglob("*")
if path.is_file()
and path.name != "build.py"
and "__pycache__" not in path.parts
and path.suffix != ".pyc"
)
with zipfile.ZipFile(BUNDLE, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for path in files:
archive.write(path, path.relative_to(SOURCE).as_posix())
print(f"wrote {BUNDLE}")
return BUNDLE
if __name__ == "__main__":
build()
Use whatever already builds your own add-on instead — scons dist, a
Makefile target, this same script. The testkit only cares that a
.nvda-addon lands somewhere the addon-bundle pattern can glob:
python examples/demo-addon/build.py
3. Look at what you’re testing
The demo add-on is deliberately boring — one gesture, one spoken phrase, one log line at startup:
"""A deliberately boring add-on: it speaks a known phrase on a known gesture,
and writes a known line to the log at startup. That is enough surface for the
kit's own end-to-end tests to assert against.
"""
from typing import ClassVar
import globalPluginHandler
import ui
from core import postNvdaStartup
from logHandler import log
STARTUP_MESSAGE = "testkit demo add-on loaded"
SPOKEN_PHRASE = "testkit demo says hello"
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
postNvdaStartup.register(self._logStartup)
def _logStartup(self):
log.info(STARTUP_MESSAGE)
def script_sayHello(self, gesture):
ui.message(SPOKEN_PHRASE)
# Read by NVDA's ScriptableObject via mangled-name getattr, not a direct reference.
__gestures: ClassVar[dict[str, str]] = {"kb:NVDA+shift+control+d": "sayHello"} # NOSONAR(S4487)
Pressing NVDA+shift+control+d speaks "testkit demo says hello".
Startup logs "testkit demo add-on loaded". That is the whole contract a
test needs to exercise.
4. Write the test
Create tests_e2e/test_first.py:
def test_demo_addon_announces_itself(nvda, addon_under_test):
before = nvda.speech.index()
nvda.keys.press("NVDA+shift+control+d")
found = nvda.speech.wait_for("testkit demo says hello", timeout=10, since=before)
assert "testkit demo says hello" in found.text
nvda.log.assert_no_errors()
What each fixture does:
| Fixture | Role in this test |
|---|---|
|
the connected client — reset between tests, so each test starts clean |
|
installs |
nvda.speech.index() before the gesture matters: wait_for only sees speech
produced after the index it was given, so capturing it after the press would
race the add-on and could miss the phrase entirely. See
nvda.speech for the full namespace.
5. Run it
pytest tests_e2e/test_first.py -v
A failure here is expected the first few times — see Troubleshooting for what a timeout, a crash, or a mismatched assertion actually looks like and what it means.
Next steps
-
Fixtures and the rest of Guide cover every namespace this test touched, plus
nvda.addons,nvda.config, andnvda.braille. -
Example Project documents the full demo add-on as a standing reference, including the two-phase install test.
-
Configuring CI gets this same test running on every pull request.
Want to help? Learn how to contribute to the ZirekHQ docs ›