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-levelAppDaemonclass.Instantiating the
AppDaemonclass can now be done without any side effects. This will also instantiate the necessary subsystem classes, but nothing will really start happening until thestart()method is called.The
AppDaemonobject is provided as a pytest fixture (ad) with thefunctionscope, which means it will be recreated fresh for each test function. The fixture handles starting and stopping theAppDaemoninstance 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
AppManagementobject.The
run_app_for_timefixture combines the functionality of theadfixture with theapp_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.
$ 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.
$ 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
$ 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.
$ 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
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
Eventto be set by the callback in the appClear the
EventSet 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
Eventagain, expecting a timeout.
- Coverage:
listen_eventresults in the callback being called for corresponding eventskeyword arguments provided in
listen_eventare passed to the callback inkwargskeyword arguments provided in the
fire_eventcall are passed to the callback indata
- 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_eventcall, 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_appapp is run until a pythonEventis set.The
Eventis 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,
Eventis 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