Testing AppDaemon

AppDaemon uses pytest and pytest-asyncio.

  • Pytest is configured in the this section in the pyproject.toml file.

    Pytest configuration options
    [tool.uv.build-backend]
    module-name = "appdaemon"
    module-root = ""
    
    # https://docs.pytest.org/en/stable/explanation/goodpractices.html
    [tool.pytest.ini_options]
    asyncio_mode = "strict"
    asyncio_default_test_loop_scope = "session"
    asyncio_default_fixture_loop_scope = "session"
    
  • Pytest-asyncio manages creating the event loop, which is normally handled by ADMain. The event loop persists throughout the test session and is reused many times by different instantiations of the top-level AppDaemon class.

  • Instantiating the AppDaemon class can now be done without any side effects. This will also instantiate the necessary subsystem classes, but nothing will really start happening until the start() method is called.

  • The AppDaemon object is provided as a pytest fixture (ad) with the function scope, which means it will be recreated fresh for each test function. The fixture handles starting and stopping the AppDaemon instance before/after each test. It also disables all apps, so they can be selectively run by the tests.

  • Apps can be modified before they’re run by the AppManagement object.

  • The run_app_for_time fixture combines the functionality of the ad fixture with the app_run_context() so that apps can be temporarily modified and run for short periods.

Running Tests

Use the uv run command to ensure uv handles the environment. It will make sure the dependencies are all satisfied.

Run all tests
  $ uv run pytest

CI Tests

The CI tests get run as part of the GitHub action on PRs to the dev branch. They’re intended to each run more or less instantly and collectively only take a few seconds.

Run CI tests
  $ uv run pytest -m ci

Functional

Functional tests cover various end-to-end interactions between components, so they require AppDaemon to be running. An example would be starting an app, having it register a callback for an event, firing that event, and checking that the callback was called. These should cover as many corner cases as possible

Run functional tests
  $ uv run pytest -m functional

Unit Tests

Unit tests don’t require AppDaemon to be running and should be run frequently during development. Currently the only unit tests are ones that cover datetime and timedelta parsing.

Run unit tests
  $ uv run pytest -m unit

Plugin Tests

Testing plugins involves either connecting to or mocking external systems, so aren’t yet covered.

Reference

Startup Tests

async tests.functional.test_startup.test_hello_world(configured_appdaemon: ConfiguredAppDaemonFunc, app_name: str) None

Run one of the hello world apps and ensure that the startup text is in the logs.

Event Tests

class tests.functional.test_event.TestEventCallback

Class to group the various tests for event callbacks.

async test_event_callback(configured_appdaemon: ConfiguredAppDaemonFunc) None

Tests the event callback functionality and the passing of kwargs through events.

Process:
  • Unique values are generated for the event and its kwargs

  • The configured_appdaemon context manager is used to run the event_test_app temporarily

  • Event test app listens for an event and fires the same event shortly after

  • Wait for Event to be set by the callback in the app

  • Clear the Event

  • Set the app arg to another event name so it will no longer match the event that was initially listened for

  • Fire the new event

  • Wait for the Event again, expecting a timeout.

Coverage:
  • listen_event results in the callback being called for corresponding events

  • keyword arguments provided in listen_event are passed to the callback in kwargs

  • keyword arguments provided in the fire_event call are passed to the callback in data

async test_event_callback_filtered(configured_appdaemon: ConfiguredAppDaemonFunc, sign: bool) None

Test the event callback filtering based on keyword arguments.

If the event data has a key that matches one of the kwargs provided in the listen_event call, then the values for those keys must also match for the callback to be executed.

Process:
  • A unique value is generated for firing the event. If the callback is supposed to fire (positive case), then the same value is used for listening to the event. Otherwise (negative case), a different, unique value is used to listen for the event, which will prevent the callback from executing.

  • The unique fire and listen values are passed to the app as args.

  • The event_test_app app is run until a python Event is set.

  • The Event is created when the app initializes.

  • The app listens for the event and then fires it after a short delay, using the relevant kwargs for each.

  • If the callback is executed, Event is set, and the unique values are printed in the logs.

Coverage:
  • Positive

    There is a matching key between the listen kwargs and the event data and the values match, so the callback is executed.

  • Negative

    There is a matching key between the listen kwargs and the event data but the values do not match, so the callback is not executed.

async test_event_callback_namespace(configured_appdaemon: ConfiguredAppDaemonFunc, sign: bool) None

Test the event callback functionality with different namespaces.

Event callbacks should only be fired for events in the correct namespace.

Process:
  • The event test app is given namespaces to listen and fire the event in.

  • The app listens for the event and then fires it after a short delay, using the relevant namespaces for each.

Coverage:
  • Positive

    The listen and fire namespaces match, so the callback is executed.

  • Negative

    The listen and fire namespaces do not match, so the callback is not executed.

async test_event_callback_oneshot(configured_appdaemon: ConfiguredAppDaemonFunc) None

Test the oneshot functionality of the event callback.

Event callbacks that are registered with the oneshot flag should only be fired once.

Process:
  • Listen for an event with the oneshot flag set.

  • Fire the event twice, and ensure that the callback is only fired once.

Coverage:
  • Event callbacks that are registered with the oneshot flag should only be fired once.

State Tests

class tests.functional.test_state.TestStateCallback

Class to group the various tests for state callbacks.

  • Tests use state_test_app.StateTestApp as the app under test
    App Args:

    listen_kwargs: Keyword arguments for the state listener (e.g., filters) state_kwargs: Keyword arguments for setting the state (e.g., new state value) delay: Delay before changing the state (default: 0.1 seconds)

  • Tests use self._run_callback_test for common logic
    • Registers a callback for a certain state change

    • Changes the state after a short delay

    • Waits for the callback to set the async Event with a timeout

async test_attribute_callback(configured_appdaemon: ConfiguredAppDaemonFunc, sign: bool) None

Test the state callback filtering based on attribute values.

State callbacks should only be fired when the specified attribute’s new value matches the filter criteria.

Parameters:
  • configured_appdaemon – Factory fixture for creating configured AppDaemon instances

  • sign – If True, the callback should fire (positive case); if False, it should not (negative case)

Process:
  • A unique value is generated for the attribute

  • If positive case, the same value is used for listening to the attribute change; if negative, a different value is used

  • The app listens for the attribute change and triggers a state change with the relevant attribute value

  • An Event is set if the callback executes

Coverage:
  • Positive: attribute’s new value matches the listen filter, callback executes

  • Negative: attribute’s new value doesn’t match the listen filter, callback doesn’t execute

async test_immediate_callback(configured_appdaemon: ConfiguredAppDaemonFunc) None

Test that the immediate flag on state listeners triggers the callback upon registration.

async test_new_state_callback(configured_appdaemon: ConfiguredAppDaemonFunc, sign: bool) None

Test the state callback filtering based on new state values.

State callbacks should only be fired when the new state matches the filter criteria.

Parameters:
  • configured_appdaemon – Factory fixture for creating configured AppDaemon instances

  • sign – If True, the callback should fire (positive case); if False, it should not (negative case)

Process:
  • A unique value is generated for the new state

  • If positive case, the same value is used for listening; if negative, a different value is used

  • The app listens for the state change and triggers it after a short delay

  • An Event is set if the callback executes

Coverage:
  • Positive: new state value matches the listen filter, callback executes

  • Negative: new state value doesn’t match the listen filter, callback doesn’t execute

async test_old_state_callback(configured_appdaemon: ConfiguredAppDaemonFunc, sign: bool) None

Test the state callback filtering based on old state values.

State callbacks should only be fired when the old state matches the filter criteria.

Parameters:
  • configured_appdaemon – Factory fixture for creating configured AppDaemon instances

  • sign – If True, the callback should fire (positive case); if False, it should not (negative case)

Process:
  • A unique value is generated for the state

  • If positive case, the same value is used for listening to old state; if negative, a different value is used

  • The app changes state twice to trigger an old state condition

  • An Event is set if the callback executes

Coverage:
  • Positive: old state value matches the listen filter, callback executes

  • Negative: old state value doesn’t match the listen filter, callback doesn’t execute