Package: appdaemon-testing
appdaemon-testingErgonomic and pythonic unit testing for AppDaemon apps. Utilities to allow you to test your AppDaemon home automation apps using all thepythonictesting patterns you are already familiar with.Installpipinstallappdaemon-testingFull DocumentationAn enhanced, source-linked version of the documentation below as well as completeAPI documentationis availablehereWriting your first testThis demo assumes you will usepytestas your test runner. Install theappdaemon-testingandpytestpackages:pipinstallappdaemon-testingpytestIn yourappdaemonconfiguration directory, introduce a newtestsdirectory. This is where we are going to write the tests for your apps.Additionally we also need to introduce an__init__.pyfile totestsandappsdirectories to make them an importable package. You should have a tree that looks something like this:├── appdaemon.yaml
├── apps
│   ├── __init__.py
│   ├── apps.yaml
│   └── living_room_motion.py
├── dashboards
├── namespaces
└── tests
    ├── __init__.py
    └── test_living_room_motion.pyWe have an automation,apps/living_room_motion.pythat we wish to test. It looks like this:importappdaemon.plugins.hass.hassapiashassclassLivingRoomMotion(hass.Hass):definitialize(self):self.listen_state(self.on_motion_detected,self.args["motion_entity"])defon_motion_detected(self,entity,attribute,old,new,kwargs):ifold=="off"andnew=="on":forlightinself.args["light_entities"]:self.turn_on(light)Create a new file,tests/test_living_room_motion.py. This is where we will write the tests for our automation.First we will declare anappdaemon_testing.pytest.automation_fixture:@automation_fixture(LivingRoomMotion,args={"motion_entity":"binary_sensor.motion_detected","light_entities":["light.1","light.2","light.3"],},)defliving_room_motion()->LivingRoomMotion:passWith this fixture, it's now possible to write some tests. We will first write a test to check the state listener callbacks are registered:deftest_callbacks_are_registered(hass_driver,living_room_motion:LivingRoomMotion):listen_state=hass_driver.get_mock("listen_state")listen_state.assert_called_once_with(living_room_motion.on_motion_detected,"binary_sensor.motion_detected")We use theappdaemon_testing.pytest.hass_driverfixture to obtain mock implementations of methods that exist on the AppDaemon Hass API. We can query these mocks and make assertions on their values. In this test we make an assertion thatlisten_stateis called once with the specified parameters.We will next write a test to make an assertion that the lights are turned on when motion is detected:deftest_lights_are_turned_on_when_motion_detected(hass_driver,living_room_motion:LivingRoomMotion):withhass_driver.setup():hass_driver.set_state("binary_sensor.motion_detected","off")hass_driver.set_state("binary_sensor.motion_detected","on")turn_on=hass_driver.get_mock("turn_on")assertturn_on.call_count==3turn_on.assert_has_calls([mock.call("light.1"),mock.call("light.2"),mock.call("light.3")])This test uses theHassDriver.setup()context manager to set the initial state for testing. When execution is withinHassDriver.setup()all state updates will not be triggered.With the initial state configured, we can now proceed to triggering the state change (HassDriver.set_state). After the state change has occured, we can then begin to make assertions about calls made to the underlying API. In this test we wish to make assertions thatturn_onis called. We obtain theturn_onmock implementation and make assertions about its calls and call count.You can see this full example and example directory structure within theexampledirectory in this repo.pytestpluginTheappdaemon_testing.pytestpackage provides a handyappdaemon_testing.pytest.hass_driverfixture to allow you easy access to the globalHassDriverinstance. This fixture takes care of ensuring AppDaemon base class methods are patched.Additionally, it provides a decorator,appdaemon_testing.pytest.automation_fixturewhich can be used to declare automation fixtures. It can be used like so:fromappdaemon_testing.pytestimportautomation_fixturefromapps.living_room_motionimportLivingRoomMotion@automation_fixture(LivingRoomMotion,args={"motion_entity":"binary_sensor.motion_detected","light_entities":["light.1","light.2","light.3"],},)defliving_room_motion()->LivingRoomMotion:pass