Skip to main content

A pytest plugin for testing Anki add-ons

Project description

pytest-anki

pytest-anki is a pytest plugin that allows developers to write tests for their Anki add-ons.

At its core lies the anki_session fixture that provides add-on authors with the ability to create and control headless Anki sessions to test their add-ons in:

from pytest_anki import AnkiSession

def test_addon_registers_deck(anki_session: AnkiSession):
    my_addon = anki_session.load_addon("my_addon")
    with anki_session.load_profile()
        with anki_session.deck_installed(deck_path) as deck_id:
            assert deck_id in my_addon.deck_ids

anki_session comes with a comprehensive API that allows developers to programmatically manipulate Anki, set up and reproduce specific configurations, simulate user interactions, and much more.

The goal is to provide add-on authors with a one-stop-shop for their functional testing needs, while also enabling them to QA their add-ons against a battery of different Anki versions, catching incompatibilities as they arise.

CI

Quickstart

Install pytest-anki with your Qt backend and Anki version:

pip install pytest-anki2[qt6,anki-2509]

Add a minimal test file:

# test_my_addon.py
from pytest_anki import AnkiSession


def test_addon_loads(anki_session: AnkiSession):
    anki_session.load_addon("my_addon")
    assert hasattr(anki_session.mw, "my_addon")


def test_with_profile(anki_session: AnkiSession):
    with anki_session.profile_loaded():
        assert anki_session.collection is not None

Run with pytest:

pytest test_my_addon.py

If you use pyproject.toml, add the plugin config:

[tool.pytest.ini_options]
qt_api = "pyqt6"

See the Usage section below for the full API reference.

Platform Support

pytest-anki is tested on Linux (Ubuntu 24.04) and macOS in CI. Linux runs the full matrix of Qt5/Qt6 and Anki versions; macOS runs a single latest-config entry.

The full test suite requires a Qt6 WebEngine ABI compatible with Ubuntu 24.04 (as used in CI). On other Linux distributions, use system Qt packages (qt6-system extra) and rely on CI for test validation. On macOS, PyPI Qt6 wheels work natively.

Installation

Requirements

  • Python 3.9+ (3.13 and 3.14 supported in CI)
  • Anki 2.1.54+ (installed automatically)
  • Qt5 or Qt6 (auto-detected at runtime; see below)

Choose your Qt backend

pytest-anki supports both PyQt5 and PyQt6, auto-detected at import time. Choose the approach that matches your system.


Option A: Ubuntu / Debian (PyPI wheels, recommended)

PyPI wheels for PyQt6, PyQt6-WebEngine, and their bundled Qt6 runtimes are built against Ubuntu's ABI and work out of the box:

pip install pytest-anki2[qt6-pypi]

With uv:

uv add --dev pytest-anki2[qt6-pypi]

Install optional selenium support for web debugging:

pip install pytest-anki2[qt6-pypi,selenium]

Option B: Arch Linux (system packages)

Use your distro's pre-compiled PyQt6 packages — they link against your system's Qt6 libraries and avoid ABI incompatibilities:

sudo pacman -S python-pyqt6-webengine
pip install pytest-anki2[qt6-system]

With uv:

sudo pacman -S python-pyqt6-webengine
uv add --dev pytest-anki2[qt6-system]

Option C: Fedora (system packages)

sudo dnf install python3-pyqt6-webengine
pip install pytest-anki2[qt6-system]

With uv:

sudo dnf install python3-pyqt6-webengine
uv add --dev pytest-anki2[qt6-system]

Option D: Any Linux with system Qt5 (fallback)

If your system provides Qt5 + PyQt5:

pip install pytest-anki2[qt5]

What's the difference between the Qt6 extras?

Extra Installs Best for
qt6 PyQt6 + PyQt6-WebEngine (bindings only) Default — use if unsure
qt6-system Same as qt6 (alias) Systems with Qt6 + WebEngine installed via native packages
qt6-pypi + PyQt6-Qt6 + PyQt6-WebEngine-Qt6 (bundled runtimes) Ubuntu / Debian where PyPI wheels work natively

Optional extras

Extra What it gives you
selenium / web Web debugging via ChromeDriver
recommended-plugins pytest-xvfb, pytest-xdist (with native forking)

Qt binding selection

pytest-anki auto-detects your installed Qt bindings at import time (PyQt6 preferred over PyQt5). To override, set the QT_API environment variable:

QT_API value Binding selected
pyqt6 or qt6 PyQt6
pyqt5 or qt5 PyQt5
QT_API=pyqt5 pytest tests/

This is useful when both PyQt5 and PyQt6 are installed, or when you want to test against a specific binding in CI.

Usage

Basic Use

The plugin registers a single anki_session fixture that launches a headless Anki instance:

from pytest_anki import AnkiSession

def test_my_addon(anki_session: AnkiSession):
    # Anki is running — interact with anki_session.mw, .app, etc.
    pass

The anki_session fixture yields an AnkiSession with these key attributes:

Attribute Type Description
mw AnkiQt Anki's main window
app AnkiApp QApplication instance
collection Collection Anki collection (after profile is loaded)
user str Profile name (default: "User 1")
base str Path to Anki's base directory
qtbot QtBot pytest-qt fixture for Qt signal testing

Profiles & collection:

def test_profile_loading(anki_session: AnkiSession):
    with anki_session.profile_loaded():
        assert anki_session.collection
        # mw.col.conf, mw.pm.profile, etc. are available

Deck management:

def test_deck_install(anki_session: AnkiSession):
    with anki_session.profile_loaded():
        with anki_session.deck_installed("path/to/deck.apkg") as deck_id:
            assert deck_id in [d.id for d in anki_session.collection.decks.all_names_and_ids()]

Loading add-ons:

def test_load_addon(anki_session: AnkiSession):
    anki_session.load_addon("my_addon_package")
    assert hasattr(anki_session.mw, "my_addon_package")

Add-on config:

def test_addon_config(anki_session: AnkiSession):
    with anki_session.addon_config_created(
        package_name="my_addon",
        default_config={"key": "default"},
        user_config={"key": "overridden"},
    ) as paths:
        pass  # config written to addons21/my_addon/config.json and meta.json

Pre-setting Anki state:

from pytest_anki import AnkiStateUpdate

def test_preset_state(anki_session: AnkiSession):
    anki_session.update_anki_state(AnkiStateUpdate(
        colconf_storage={"my_key": True},
        profile_storage={"my_key": True},
    ))

Running tasks in the Qt event loop:

def test_threaded_task(anki_session: AnkiSession):
    result = anki_session.run_in_thread_and_wait(
        lambda: 42, timeout=5000
    )
    assert result == 42

Configuring the Anki Session

Customize the session using the dedicated marker (recommended):

import pytest

@pytest.mark.anki_session(
    load_profile=True,
    profile_name="CustomUser",
    lang="de_DE",
    packed_addons=["path/to/addon.ankiaddon"],
    unpacked_addons=[("my_addon", "path/to/addon/source")],
    addon_configs=[("my_addon", {"key": "value"})],
    preset_anki_state=AnkiStateUpdate(meta_storage={"key": True}),
    enable_web_debugging=False,
    skip_loading_addons=False,
)
def test_configured_session(anki_session: AnkiSession):
    assert anki_session.mw.pm.name == "CustomUser"

The equivalent via indirect parametrization also works:

@pytest.mark.parametrize("anki_session", [dict(load_profile=True)], indirect=True)
def test_via_parametrize(anki_session: AnkiSession):
    ...
Parameter Type Default Description
base_path str system tempdir Directory for Anki base folder
base_name str "anki_base" Base folder name
profile_name str "User 1" User profile name
lang str "en_US" Profile language
load_profile bool False Pre-load profile/collection
preset_anki_state AnkiStateUpdate None Pre-configure col/prof/meta storage
packed_addons List[Path] None .ankiaddon packages to install
unpacked_addons List[Tuple[str, Path]] None Source folders to install as add-ons
addon_configs List[Tuple[str, dict]] None Config key/value pairs for add-ons
enable_web_debugging bool False Enable remote devtools
skip_loading_addons bool False Install but don't auto-load add-ons

Module-scoped session

For suites with many tests that don't mutate global state, use the module-scoped variant:

def test_first(anki_session_module: AnkiSession):
    assert anki_session_module.mw is not None

def test_second(anki_session_module: AnkiSession):
    # Shares the same Anki process as test_first
    pass

anki_session_module accepts the same parameters as anki_session. Use reset_state() between tests to clear addon modules:

def test_isolated(anki_session_module: AnkiSession):
    anki_session_module.load_addon("my_addon")
    anki_session_module.reset_state()  # clears sys.modules

When using module-scoped sessions, disable auto-forking with --anki-no-fork or anki_force_fork = false to share a single process.

Isolated addon loading

The loaded_addon context manager restores hook registries on exit, preventing state leakage between tests when running without forks:

def test_isolated_addon(anki_session: AnkiSession):
    with anki_session.loaded_addon("my_addon") as mod:
        assert mod.some_function()
    # Hook registries are restored to pre-import state

Web debugging

When enable_web_debugging=True, you can drive Anki's web views via Selenium:

from pytest_anki import AnkiWebViewType

def test_web_view(anki_session: AnkiSession):
    with anki_session.profile_loaded():
        anki_session.run_with_chrome_driver(
            lambda driver: driver.find_element("tag name", "body"),
            target_web_view=AnkiWebViewType.main_webview,
        )

Migration Guide (v1 → v2)

v2.0.0 is a major rewrite. Here's what changed:

Breaking changes

v1 v2
pytest-anki2[qt5] / pytest-anki2[qt6] pytest-anki2[qt5] / pytest-anki2[qt6] / pytest-anki2[qt6-system] / pytest-anki2[qt6-pypi]
Poetry-based build uv + hatchling
Python 3.7+ Python 3.9+
pytest-forked xdist native forking (pytest-xdist>=3.0)
Manual forking required Auto-forked by default (opt-out via --anki-no-fork)
PyQt5 only PyQt6 auto-detected, PyQt5 fallback
Selenium always installed Selenium optional ([selenium] extra)

What to update

  1. Switch to uv (or keep pip — pip install pytest-anki2[...] still works).
  2. Specify an Anki version extra: pip install pytest-anki2[anki-2509,qt6].
  3. Remove pytest-forked from your dependencies — xdist handles forking.
  4. If you used @pytest.mark.forked explicitly, it's now automatic; use --anki-no-fork to disable.
  5. If you relied on pytest-anki pulling in selenium, add selenium extra explicitly.

What stayed the same

  • The anki_session fixture API is backward-compatible.
  • AnkiSession, AnkiStateUpdate, AnkiWebViewType are at pytest_anki.
  • Indirect parametrization via @pytest.mark.parametrize("anki_session", [...], indirect=True) works as before.

Additional Notes

When to use pytest-anki

Running your test in an Anki environment is expensive and introduces an additional layer of confounding factors. If you can mock your Anki runtime dependencies away, then that should always be your first tool of choice.

Where anki_session comes in handy is further towards the upper levels of the test pyramid, i.e. functional tests, end-to-end tests, and UI tests. Additionally the plugin can provide you with a convenient way to automate testing for incompatibilities with Anki and other add-ons.

The importance of forking your tests

Since v2.0.0, all tests using this plugin are automatically marked as forked (via pytest_collection_modifyitems). This is because, while the plugin does attempt to tear down Anki sessions as cleanly as possible on exit, this process is never quite perfect, especially for add-ons that monkey-patch Anki.

With unforked test runs, factors like that can lead to unexpected behavior, or worse still, your tests crashing. Forking a new subprocess for each test bypasses these limitations.

When running without forks, the loaded_addon() context manager now also snapshots and restores aqt.gui_hooks and anki.hooks registries, preventing addon hook registrations from leaking between tests.

To opt out of automatic forking, set anki_force_fork = false in your pyproject.toml:

[tool.pytest.ini_options]
anki_force_fork = false

Or pass --anki-no-fork on the command line:

pytest --anki-no-fork tests/

Disabling forking can speed up test suites that share a single Anki process, but may cause instability if add-ons mutate global state.

Automated Testing

pytest-anki is designed to work well with continuous integration systems such as GitHub actions. For an example see pytest-anki's own GitHub workflows.

Troubleshooting

Qt6 ABI incompatibility

PyPI wheels for PyQt6-WebEngine ship Qt6 libraries compiled against Ubuntu's ABI. On Arch, Fedora, or other non-Ubuntu distros:

# Use system Qt packages
sudo pacman -S python-pyqt6-webengine   # Arch
sudo dnf install python3-pyqt6-webengine # Fedora
pip install pytest-anki2[qt6-system]

If you cannot run the full suite locally, CI (GitHub Actions) is the authoritative validation. You can always run lint and type checks:

make lint
make check

pytest hangs when using xvfb

Blocking non-dismissable prompts from your add-on code can cause hangs. Bypass xvfb temporarily:

pytest --no-xvfb tests/

ImportError: No Qt bindings found

Install a Qt backend:

pip install pytest-anki2[qt6]
# or for PyQt5:
pip install pytest-anki2[qt5]

If you have Qt installed but the wrong one is selected, set QT_API:

QT_API=pyqt5 pytest tests/

Tests fail with "Failed to import add-on"

Ensure your add-on is in Anki's addons21 directory, or use unpacked_addons to point to a source folder during session setup:

@pytest.mark.parametrize("anki_session", [dict(
    unpacked_addons=[("my_addon", "/path/to/source")],
)], indirect=True)
def test_with_addon(anki_session: AnkiSession):
    anki_session.load_addon("my_addon")

Contributing

Contributions are welcome! To set up pytest-anki for development, please first make sure you have Python 3.9+ and uv installed, then run the following steps:

$ git clone https://github.com/Alexander-Nilsson/pytest-anki.git

$ cd pytest-anki

$ make install

Before submitting any changes, please make sure that pytest-anki's checks and tests pass:

make lint
make check
make test      # requires Ubuntu-compatible Qt6 ABI; CI runs the full matrix
# or use Docker to match the CI environment:
make test-docker
# or run just the unit tests (no Qt6 ABI needed):
make test-light

This project uses ruff to enforce a consistent code style. To auto-format your code you can use:

make format

License and Credits

pytest-anki is

Copyright © 2026 Alexander Nilsson

Copyright © 2019-2025 Aristotelis P. (Glutanimate) and contributors

Copyright © 2017-2019 Michal Krassowski

Copyright © 2017-2021 Ankitects Pty Ltd and contributors

All credits for the original idea for creating a context manager to test Anki add-ons with go to Michal. pytest-anki would not exist without his anki_testing project.

I would also like to extend a heartfelt thanks to AMBOSS for their major part in supporting the development of this plugin! Most of the recent feature additions leading up to v1.0.0 of the plugin were implemented as part of my work on the AMBOSS add-on.

pytest-anki is free and open-source software. Its source-code is released under the GNU AGPLv3 license, extended by a number of additional terms. For more information please see the license file that accompanies this program.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. Please see the license file for more details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_anki2-2.2.0.tar.gz (355.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_anki2-2.2.0-py3-none-any.whl (55.5 kB view details)

Uploaded Python 3

File details

Details for the file pytest_anki2-2.2.0.tar.gz.

File metadata

  • Download URL: pytest_anki2-2.2.0.tar.gz
  • Upload date:
  • Size: 355.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_anki2-2.2.0.tar.gz
Algorithm Hash digest
SHA256 8151fe30a8c226ef8d598a229f01ae88c59cc53f8eea24fe00ecf2c423fce880
MD5 8a46f997cc55d38df9cd11c89aa2a93b
BLAKE2b-256 f102a0242baa4876516646c7912a3afeb80f7734b3b01122354695f749f1fb55

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_anki2-2.2.0.tar.gz:

Publisher: release.yml on Alexander-Nilsson/pytest-anki

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_anki2-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: pytest_anki2-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 55.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_anki2-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5380ee0be821ad0635ec519d35ca25f33c85a7b5db546ea70d24287597b2c6ee
MD5 2e0d45752a318a841b1ae666789e7144
BLAKE2b-256 1d02ef7a31831f7623ef3745de8632bec9611786aa537862a57a6d89923c5838

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_anki2-2.2.0-py3-none-any.whl:

Publisher: release.yml on Alexander-Nilsson/pytest-anki

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page