{"id":15095831,"url":"https://github.com/hellothisisflo/appdaemon-test-framework","last_synced_at":"2025-04-14T23:32:04.493Z","repository":{"id":32730610,"uuid":"141160631","full_name":"HelloThisIsFlo/Appdaemon-Test-Framework","owner":"HelloThisIsFlo","description":"Clean, human-readable tests for Appdaemon","archived":false,"fork":false,"pushed_at":"2024-01-11T20:59:58.000Z","size":524,"stargazers_count":45,"open_issues_count":12,"forks_count":19,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-24T20:38:23.026Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://hellothisisflo.github.io/Appdaemon-Test-Framework/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HelloThisIsFlo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-16T15:51:02.000Z","updated_at":"2024-10-01T10:59:06.000Z","dependencies_parsed_at":"2024-06-20T22:08:04.915Z","dependency_job_id":null,"html_url":"https://github.com/HelloThisIsFlo/Appdaemon-Test-Framework","commit_stats":null,"previous_names":["floriankempenich/appdaemon-test-framework"],"tags_count":30,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HelloThisIsFlo%2FAppdaemon-Test-Framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HelloThisIsFlo%2FAppdaemon-Test-Framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HelloThisIsFlo%2FAppdaemon-Test-Framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HelloThisIsFlo%2FAppdaemon-Test-Framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HelloThisIsFlo","download_url":"https://codeload.github.com/HelloThisIsFlo/Appdaemon-Test-Framework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248978963,"owners_count":21192881,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-09-25T15:42:25.739Z","updated_at":"2025-04-14T23:32:04.120Z","avatar_url":"https://github.com/HelloThisIsFlo.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Appdaemon Test Framework\n[![Build Status](https://www.travis-ci.com/FlorianKempenich/Appdaemon-Test-Framework.svg?branch=master)](https://travis-ci.com/github/FlorianKempenich/Appdaemon-Test-Framework) [![PyPI](https://img.shields.io/pypi/v/appdaemontestframework.svg)](https://pypi.org/project/appdaemontestframework/)\n\nClean, human-readable tests for your Appdaemon automations.\n\n* Totally transparent, No code modification is needed.\n* Mock the state of your home: `given_that.state_of('sensor.temperature').is_set_to('24.9')`\n* Seamless assertions: `assert_that('light.bathroom').was.turned_on()`\n* Simulate time: `time_travel.fast_forward(2).minutes()`\n\n##### How does it look?\n```python\ndef test_during_night_light_turn_on(given_that, living_room, assert_that):\n    given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night\n    living_room._new_motion(None, None, None)\n    assert_that('light.living_room').was.turned_on()\n\ndef test_click_light_turn_on_for_5_minutes(given_that, living_room, assert_that):\n    living_room._new_button_click(None, None, None)\n    assert_that('light.bathroom').was.turned_on()\n\n    # At T=4min light should not yet have been turned off\n    time_travel.fast_forward(4).minutes()\n    assert_that('light.bathroom').was_not.turned_off()\n\n    # At T=5min light should have been turned off\n    time_travel.fast_forward(1).minutes()\n    time_travel.assert_current_time(5).minutes()\n    assert_that('light.bathroom').was.turned_off()\n\n```\n\n---\n## Table of Contents\n\n- [5-Minutes Quick Start Guide](#5-minutes-quick-start-guide)\n  * [Initial Setup](#initial-setup)\n  * [Write you first unit test](#write-you-first-unit-test)\n  * [Result](#result)\n- [General Test Flow and Available helpers](#general-test-flow-and-available-helpers)\n  * [0. Initialize the automation: `@automation_fixture`](#0-initialize-the-automation-automation_fixture)\n  * [1. Set the stage to prepare for the test: `given_that`](#1-set-the-stage-to-prepare-for-the-test-given_that)\n  * [2. Trigger action on your automation](#2-trigger-action-on-your-automation)\n  * [3. Assert on your way out: `assert_that`](#3-assert-on-your-way-out-assert_that)\n  * [Bonus — Assert callbacks were registered during `initialize()`](#bonus--assert-callbacks-were-registered-during-initialize)\n  * [Bonus — Travel in Time: `time_travel`](#bonus--travel-in-time-time_travel)\n- [Examples](#examples)\n- [Under The Hood](#under-the-hood)\n- [Advanced Usage](#advanced-usage)\n- [Contributing](#contributing)\n- [Author Information](#author-information)\n\n---\n\n## 5-Minutes Quick Start Guide\n### Initial Setup\n1. Install **pytest**: `pip install pytest`\n1. Install the **framework**: `pip install appdaemontestframework`\n1. Create a `conftest.py` file in the **root** of your project with the following code:\n   ```python\n   from appdaemontestframework.pytest_conftest import *\n   ```\n\n### Write you first unit test\nLet's test an Appdaemon automation we created, which, say, handles automatic lighting in the Living Room: `class LivingRoom`\n\u003c!-- We called the class `LivingRoom`. Since it's an Appdaemon automation, its lifecycle is handled  --\u003e\n\n1. **Initialize** the Automation Under Test with the `@automation_fixture` decorator:\n   ```python\n   @automation_fixture(LivingRoom)\n   def living_room():\n     pass\n   ```\n1. **Write your first test:**\n   ##### Our first unit test\n   ```python\n   def test_during_night_light_turn_on(given_that, living_room, assert_that):\n       given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night\n       living_room._new_motion(None, None, None)\n       assert_that('light.living_room').was.turned_on()\n   ```\n   \u003e ##### Note\n   \u003e The following fixtures are **injected** by pytest using the **[`conftest.py`](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/new-features/doc/full_example/conftest.py) file** and the **initialisation fixture created at Step 1**:\n   \u003e * `living_room`\n   \u003e * `given_that`\n   \u003e * `assert_that`\n   \u003e * `time_travel`\n\n### Result\n```python\n# Important:\n# For this example to work, do not forget to copy the `conftest.py` file.\n\n@automation_fixture(LivingRoom)\ndef living_room():\n    pass\n\ndef test_during_night_light_turn_on(given_that, living_room, assert_that):\n    given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night\n    living_room._new_motion(None, None, None)\n    assert_that('light.living_room').was.turned_on()\n\ndef test_during_day_light_DOES_NOT_turn_on(given_that, living_room, assert_that):\n    given_that.state_of('sensor.living_room_illumination').is_set_to(1000) # 1000lm == sunlight\n    living_room._new_motion(None, None, None)\n    assert_that('light.living_room').was_not.turned_on()\n```\n\n\n---\n## General Test Flow and Available helpers\n### 0. Initialize the automation: `@automation_fixture`\n```python\n# Command\n@automation_fixture(AUTOMATION_CLASS)\ndef FIXTURE_NAME():\n    pass\n\n# Example\n@automation_fixture(LivingRoom)\ndef living_room():\n    pass\n```\nThe automation given to the fixture will be:\n* **Created**\n  _Using the required mocks provided by the framework_\n* **Initialized**\n  _By calling the `initialize()` function_\n* **Made available as an injectable fixture**\n   _Just like a regular `@pytest.fixture`_\n\n### 1. Set the stage to prepare for the test: `given_that`\n*    #### Simulate args passed via `apps.yaml` config\n     See: [Appdaemon - Passing arguments to Apps](http://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#passing-arguments-to-apps)\n     ```python\n     # Command\n     given_that.passed_arg(ARG_KEY).is_set_to(ARG_VAL)\n\n     # Example\n     given_that.passed_arg('color').is_set_to('blue')\n     ```\n\n     Note: See [Pre-initialization Setup](#pre-initialization-setup) if\n     arguments are required in the `initialize()` method.\n\n*    #### State\n     ```python\n     # Command\n     given_that.state_of(ENTITY_ID).is_set_to(STATE_TO_SET)\n     given_that.state_of(ENTITY_ID).is_set_to(STATE_TO_SET, ATTRIBUTES_AS_DICT)\n     given_that.state_of(ENTITY_ID).is_set_to(STATE_TO_SET,\n                                              ATTRIBUTES_AS_DICT,\n                                              LAST_UPDATED_AS_DATETIME,\n                                              LAST_CHANGED_AS_DATETIME)\n\n     # Example\n     given_that.state_of('media_player.speaker').is_set_to('playing')\n     given_that.state_of('light.kitchen').is_set_to('on', {'brightness': 50,\n                                                           'color_temp': 450})\n\n     given_that.state_of('light.kitchen').is_set_to(\n                                            'on', \n                                            last_updated=datetime(2020, 3, 3, 11, 27))\n     ```\n\n*    #### Time\n     ```python\n     # Command\n     given_that.time_is(TIME_AS_DATETIME)\n\n     # Example\n     given_that.time_is(time(hour=20))\n     ```\n\n*    #### Extra\n     ```python\n     # Clear all calls recorded on the mocks\n     given_that.mock_functions_are_cleared()\n\n     # To also clear all mocked state, use the option: 'clear_mock_states'\n     given_that.mock_functions_are_cleared(clear_mock_states=True)\n\n     # To also clear all mocked passed args, use the option: 'clear_mock_passed_args'\n     given_that.mock_functions_are_cleared(clear_mock_passed_args=True)\n     ```\n\n### 2. Trigger action on your automation\nThe way Automations work in Appdaemon is:\n* First you **register callback methods** during the `initialize()` phase\n* At some point **Appdaemon will trigger these callbacks**\n* Your Automation **reacts to the call on the callback**\n\nTo **trigger actions** on your automation, simply **call one of the registered callbacks**.\n\n\u003e #### Note\n\u003e It is best-practice to have an initial test that will test the callbacks\n\u003e are _actually_ registered as expected during the `initialize()` phase.\n\u003e See: [Bonus - Assert callbacks were registered during `initialize()`](#bonus--assert-callbacks-were-registered-during-initialize)\n\n#### Example\n\n##### `LivingRoomTest.py`\n```python\ndef test_during_night_light_turn_on(given_that, living_room, assert_that):\n   ...\n   living_room._new_motion(None, None, None)\n   ...\n```\n\n##### With `LivingRoom.py`\n```python\nclass LivingRoom(hass.Hass):\n    def initialize(self):\n        ...\n        self.listen_event(\n                self._new_motion,\n                'motion',\n                entity_id='binary_sensor.bathroom_motion')\n        ...\n\n    def _new_motion(self, event_name, data, kwargs):\n        \u003c Handle motion here \u003e\n```\n\n\n\n### 3. Assert on your way out: `assert_that`\n\n*    #### Entities\n     ```python\n     # Available commmands\n     assert_that(ENTITY_ID).was.turned_on(OPTIONAL_KWARGS)\n     assert_that(ENTITY_ID).was.turned_off(OPTIONAL_KWARGS)\n     assert_that(ENTITY_ID).was_not.turned_on(OPTIONAL_KWARGS)\n     assert_that(ENTITY_ID).was_not.turned_off(OPTIONAL_KWARGS)\n\n     # Examples\n     assert_that('light.living_room').was.turned_on()\n     assert_that('light.living_room').was.turned_on(color_name=SHOWER_COLOR)\n     assert_that('light.living_room').was_not.turned_off()\n     assert_that('light.living_room').was_not.turned_off(transition=2)\n     ```\n\n*    #### Services\n     ```python\n     # Available commmands\n     assert_that(SERVICE).was.called_with(OPTIONAL_KWARGS)\n     assert_that(SERVICE).was_not.called_with(OPTIONAL_KWARGS)\n\n     # Examples\n     assert_that('notify/pushbullet').was.called_with(\n                         message='Hello :)',\n                         target='My Phone')\n\n     assert_that('media_player/volume_set').was.called_with(\n                         entity_id='media_player.bathroom_speaker',\n                         volume_level=0.6)\n     ```\n\n\n### Bonus — Assert callbacks were registered during `initialize()`\n*    #### Listen: Event\n     ```python\n     # Available commmands\n     assert_that(AUTOMATION) \\\n         .listens_to.event(EVENT) \\\n         .with_callback(CALLBACK)\n     assert_that(AUTOMATION) \\\n         .listens_to.event(EVENT, OPTIONAL_KWARGS) \\\n         .with_callback(CALLBACK)\n\n     # Examples - Where 'living_room' is an instance of 'LivingRoom' automation\n     assert_that(living_room) \\\n         .listens_to.event('click', entity_id='binary_sensor.button') \\\n         .with_callback(living_room._new_click_button)\n     ```\n\n*    #### Listen: State\n     ```python\n     # Available commmands\n     assert_that(AUTOMATION) \\\n         .listens_to.state(ENTITY_ID) \\\n         .with_callback(CALLBACK)\n     assert_that(AUTOMATION) \\\n         .listens_to.state(ENTITY_ID, OPTIONAL_KWARGS) \\\n         .with_callback(CALLBACK)\n\n     # Examples - Where 'living_room' is an instance of 'LivingRoom' automation\n     assert_that(living_room) \\\n         .listens_to.state('binary_sensor.button', old='on', new='off') \\\n         .with_callback(living_room._no_more_motion)\n     ```\n\n*    #### Registered: Run Daily\n     ```python\n     # Available commmands\n     assert_that(AUTOMATION) \\\n         .registered.run_daily(TIME_AS_DATETIME) \\\n         .with_callback(CALLBACK)\n     assert_that(AUTOMATION) \\\n         .registered.run_daily(TIME_AS_DATETIME, OPTIONAL_KWARGS) \\\n         .with_callback(CALLBACK)\n\n     # Examples - Where 'living_room' is an instance of 'LivingRoom' automation\n     assert_that(living_room) \\\n         .registered.run_daily(time(hour=12), time_as_text='noon') \\\n         .with_callback(automation._new_time)\n     ```\n\n     **NOTE:** The above examples can be used with run_minutely.\n\n_See related test file for more examples: [test_assert_callback_registration.py](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/test/test_assert_callback_registration.py)_\n\n\n### Bonus — Travel in Time: `time_travel`\nThis helper simulate going forward in time.\n\nIt will run the callbacks registered with the `run_in`function of Appdaemon:\n* **Order** is kept\n* Callback is run **only if due** at current simulated time\n* **Multiples calls** can be made in the same test\n* Automatically **resets between each test** _(with default config)_\n\n\n ```python\n# Available commmands\n\n## Simulate time\ntime_travel.fast_forward(MINUTES).minutes()\ntime_travel.fast_forward(SECONDS).seconds()\n\n## Assert time in test — Only useful for sanity check\ntime_travel.assert_current_time(MINUTES).minutes()\ntime_travel.assert_current_time(SECONDS).seconds()\n\n\n\n# Example\n\n# 2 services:\n#   * 'first/service': Should be called at T=3min\n#   * 'second/service': Should be called at T=5min\ntime_travel.assert_current_time(0).minutes()\n\ntime_travel.fast_forward(3).minutes()\nassert_that('some/service').was.called()\nassert_that('some_other/service').was_not.called()\n\ntime_travel.fast_forward(2).minutes()\nassert_that('some_other/service').was.called()\n```\n\n---\n## Examples\n### Simple\n*  [**Pytest**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/pytest_example.py)\n*  [**Unittest**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/unittest_example.py)\n\n### Special cases\n#### Complex setup fixture\nIn this scenario, we wish to test what happens **after** the light was turned on by motion and the `device_tracker.person` is set to `away`.\nAfter the first `_new_motion`, the mock functions must be reset to ensure a clean state. For that purpose, it is entirely possible to use `given_that.mock_functions_are_cleared()` inside a particular test:\n```python\ndef test_light_turned_on_by_motion_and_person_away__do_not_turn_on_again(given_that,\n                                                                         living_room,\n                                                                         assert_that):\n  # Given:\n  # - Light was turned on by motion\n  given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night\n  living_room._new_motion(None, None, None)\n  given_that.mock_functions_are_cleared()\n  # - Person is away\n  given_that.state_of('device_tracker.person).is_set_to('away')\n\n\n  # When: New motion\n  living_room._new_motion(None, None, None)\n\n\n  # Then: Light isn't turned on\n  assert_that('light.living_room').was_not.turned_on()\n```\n\nAlternatively, if multiple scenarios are to be tested with the same fixture (after turned on by motion \u0026 person is away), an intermediate fixture can be used:\n\n```python\nclass AfterLightTurnedOnByMotion:\n\n  @pytest.fixture\n  def living_room_after_light_turned_on_by_motion(self, living_room, given_that):\n    given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night\n    living_room._new_motion(None, None, None)\n    given_that.mock_functions_are_cleared()\n\n  def test_person_away__new_motion__do_not_turn_on_again(self,\n                                                         given_that,\n                                                         living_room_after_light_turned_on_by_motion,\n                                                         assert_that):\n    given_that.state_of('device_tracker.person).is_set_to('away')\n    living_room_after_light_turned_on_by_motion._new_motion(None, None, None)\n    assert_that('light.living_room').was_not.turned_on()\n\n  def test_some_other_condition__do_something_else(self,\n                                                   given_that,\n                                                   living_room_after_light_turned_on_by_motion,\n                                                   assert_that):\n    ...\n\n```\n\n\n### [Complete Project](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/tree/master/doc/full_example)\n* **Kitchen**\n  * [**Automation**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/full_example/apps/kitchen.py)\n  * [**Tests**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/full_example/tests/test_kitchen.py)\n* **Bathroom**\n  * [**Automation**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/full_example/apps/bathroom.py)\n  * [**Tests**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/full_example/tests/test_bathroom.py)\n\n---\n\n## Under The Hood\n\u003e This section is **entirely optional**\n\u003e For a guide on how to use the framework, see the above sections!\n\n### Understand the motivation\n**Why a test framework dedicated for Appdaemon?**\n_The way Appdaemon allow the user to implement automations is based on inheritance.\nThis makes testing not so trivial.\nThis test framework abstracts away all that complexity, allowing for a smooth TDD experience._\n\n**Couldn't we just use the MVP pattern with clear interfaces at the boundaries?**\n_Yes we could... but would we?\nLet's be pragmatic, with this kind of project we're developing for our home, and we're a team of one.\nWhile being a huge proponent for [clean architecture](https://floriankempenich.com/post/11), I believe using such a complex architecture for such a simple project would only result in bringing more complexity than necessary._\n\n### Enjoy the simplicity\n\nEvery Automation in Appdaemon follows the same model:\n* **Inherit** from **`hass.Hass`**\n* **Call** Appdaemon API through **`self`**\n\n**AppdaemonTestFramework** captures all calls to the API and helpers make use of the information to implement common functionality needed in our tests.\n\nMethods from the `hass.Hass` class are patched globally in the `HassMocks` class and the mocks can be accessed with the `hass_functions` property. The test fixture is injected in the helper classes as `hass_mocks`.\nAfter all tests have run, the `hass_mocks` test fixture will automatically unpatch all the mocks.\n\n\u003e **Deprecated `hass_functions` test fixture**\n\u003e Before `HassMocks` existed, `hass_functions` was used for interacting with the patch hass methods. The `hass_mocks` test fixture now contains `hass_functions` and should be accessed through there.\n\u003e A `hass_functions` test fixture that returns `hass_mocus.hass_fuctions` will be kept for a while to ease transition, but wil generate a deprecation warning.\n\n1. **`hass_functions`**: **dictionary** with all patched functions\n1. **`unpatch_callback`**: **callback** to un-patch all patched functions\n\n**`hass_functions`** are injected in the helpers when creating their instances.\nAfter all tests, **`unpatch_callback`** is used to un-patch all patched functions.\n\nSetup and teardown are handled in the [`pytest_conftest.py`](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/appdaemontestframework/pytest_conftest.py) file.\n\n\n#### Appdaemon Test Framework flow\n###### 1. Setup\n* **Patch** `hass.Hass` functions and create `HassMocks` instance for tracking state.\n* **Inject** `hass_mocks` in helpers: `given_that`, `assert_that`, `time_travel`\n###### 2. Test run\n* **Run** the test suite\n###### 3. Teardown\n* **Un-patch** `hass.Hass` functions\n\n### Feature focus\n#### `@automation_fixture`\n\nTo be able to test an Appdaemon automation, the automation needs to be created **after** the framework has been\ninitialized. This could be done with a regular `@pytest.fixture` by injecting `given_that` (or any other helper) before\ncreating the automation. This would invoke the helper which would initialize the framework beforehand and the correct\nfunctions would be patched before the automation is created:\n```python\n@pytest.fixture\ndef living_room(given_that):\n    # `None` dependencies are mocked by the framework\n    living_room = LivingRoom(None, None, None, None, None, None, None, None)\n    return living_room\n```\n\nAlso, for convenience, we could initialize the automation with its `initialize()` method to make it available in tests\nas it would be in production once Appdaemon is started, making sure the mocks are clear of any calls when the fixture\nis injected. We could also set the automation instance name to the name of the automation class.\n```python\n@pytest.fixture\ndef living_room(given_that):\n    living_room = LivingRoom(None, LivingRoom.__name__, None, None, None, None, None, None)\n    living_room.initialize()\n    given_that.mock_functions_are_cleared()\n    return living_room\n```\n\nHowever, since this code would have to be repeated for every single automation, it was creating un-necessary clutter.\nFor that reason, the `@automation_fixture` was introduced. It is simple syntactic sugar, and performs the exact same\nsteps as the fixture above in much fewer lines:\n\n```python\n@automation_fixture(LivingRoom)\ndef living_room():\n    pass\n```\n\n\n## Advanced Usage\n\n### `@automation_fixture` - Extra features\n* #### Pre-initialization setup\n  For some automations, the `initialize()` step requires some pre-configuration of the global state.\n  Maybe it requires time to be setup, or maybe it needs some sensors to have a particular state.\n  Such pre-initialization setup is possible with the `@automation_fixture`. The fixture can be injected with the\n  following 2 arguments:\n  * `given_that` _- For configuring the state_\n  * `hass_mocks` _- For more complex setup steps_\n    * Note: `hass_functions` was deprecated in favor of using `hass_mocks`\n\n  Any code written in the fixture will be executed **before** initializing the automation. That way your\n  `initialize()` function can safely rely on the Appdaemon framework and call some of its methods, all you\n  have to do is setup the context in the fixture.\n\n  ##### Example\n  Let's imagine an automation, `Terasse`, that turns on the light at night. During the `initialize()` phase, `Terasse`\n  checks the current time to know if it should immediately turn on the light (for instance, if appdaemon is started\n  during the night).\n  Without mocking the time **before** the call to `initialize()` the test framework will not know which time to return\n  and an error will be raised. To prevent that, we set the time in the fixture, the code will be executed **before** the\n  call to `initialize()` and no error will be raised.\n  ```python\n  @automation_fixture\n  def terasse(given_that):\n      given_that.time_is(datetime.time(hour=20))\n\n\n  # With Terasse:\n  class Terasse(hass.Hass):\n      def initialize(self):\n          ...\n          current_time = self.time()\n          if self._is_during_night(current_time):\n              self._turn_on_light()\n          ...\n  ```\n\n* #### Alternate versions\n  The `@automation_fixture` can actually be used in 4 different ways:\n  ```python\n  # Single Class:\n  @automation_fixture(MyAutomation)\n\n  # Multiple Classes:\n  @automation_fixture(MyAutomation, MyOtherAutomation)\n\n  # Single Class w/ params:\n  @automation_fixture((upstairs.Bedroom, {'motion': 'binary_sensor.bedroom_motion'}))\n\n  # Multiple Classes w/ params:\n  @automation_fixture(\n    (upstairs.Bedroom, {'motion': 'binary_sensor.bedroom_motion'}),\n    (upstairs.Bathroom, {'motion': 'binary_sensor.bathroom_motion'}),\n  )\n  ```\n\n  The alternate versions can be useful for parametrized testing:\n  * When multiple classes are passed, tests will be generated for each automation.\n  * When using parameters, the injected object will be a tuple: `(Initialized_Automation, params)`\n\n### Deprecation Warnings\nAs development continues of this test framework, some interfaces and test fixtures need to get\ndeprecated. In general, the following method is used to ease the transitions\n\n1. Mark deprecated calls with a warning\n1. Provide an expressive message directing you how to change your code\n1. At least one minor release will include the warning\n1. In a future MAJOR release, this warning will change to an error and/or the deprecated code will be\n   removed all together.\n\nThis deprecation strategy allows you to keep using Appdaemon Test Framework as you always have, as long as you don't update to a new MAJOR version.\n\n\n**Silencing deprecation warnings**\n\nThe deprecation warnings can be a bit overwhelming depending on the current state of the codebase.\nIf you would like to run tests and ignore these warnings use the following pytest options:\n\n```sh\npytest -W ignore::DeprecationWarning\n```\n\n### Without `pytest`\nIf you do no wish to use `pytest`, first maybe reconsider, `pytest` is awesome :)\nIf you're really set on using something else, worry not it's pretty straighforward too ;)\n\nWhat the [`conftest.py`](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/new-features/doc/full_example/conftest.py) file is doing is simply handling the setup \u0026 teardown, as well as providing the helpers as injectable fixtures.\nIt is pretty easy to replicate the same behavior with your test framework of choice. For instance, with `unittest` a base `TestCase` can replace pytest [`conftest.py`](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/new-features/doc/full_example/conftest.py). See the [Unittest Example](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/doc/unittest_example.py)\n\n### Direct call to mocked functions\n\u003e /!\\ WARNING — EXPERIMENTAL /!\\\n\n**Want a functionality not implemented by the helpers?**\nYou can inject `hass_mocks` directly in your tests and use `hass_mocks.hass_functions`, patched functions are `MagicMocks`.\nThe list of patched functions can be found in the [**`hass_mocks` module**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/master/appdaemontestframework/hass_mocks.py#L20framework.py#L14).\n\n\n---\n## Contributing\n\n* **All contributions are welcome!**\n* **All PR must be accompanied with some tests showcasing the new feature**\n* **For new feature, a short discussion will take place to argue whether the feature makes sense and if this is the best place to implement**\n\n* **Then the process goes as follow:**\n  * Is readable? ✅\n  * Passes the tests? ✅\n  * I'm merging 🎉🎉\n\n\n\u003e PR simply patching new functions in `hass_functions`\n\u003e   * Will be merged almost immediately\n\u003e   * Are exempt of the _\"must have tests\"_ rule, although there is rarely too much tests 😉\n\nThanks for your contribution 😀 👍\n\n\n### Tests\n\nWhen adding new feature, you can TDD it, add unit tests later, or only rely on integration tests. Whichever way you go is totally fine for this project, but new features will need to have at least some sort of tests, even if they're super basic 🙂\n\n### Using `Pipenv`\nThis project has been developed using `Pipenv`.\nIt is totally optional, but if you are interested, here is a quick guide to get you started:\n1. Install `pipenv` -\u003e `brew install pipenv` (or whichever methods works best for you)\n2. `cd` in the project directory\n3. Run `pipenv install --dev`  \n  _That will create a `virtualenv` and automatically install all required dependencies_\n4. Run `pipenv shell`  \n  _That will activate the `virtualenv` in the current shell_\n5. Run `pytest` to ensure that everything is working as expected\n6. You're good to go and can start contributing 😊\n\n---\n## Author Information\nFollow me on Twitter: [@ThisIsFlorianK](https://twitter.com/ThisIsFlorianK)\nFind out more about my work: [Florian Kempenich — Personal Website](https://floriankempenich.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhellothisisflo%2Fappdaemon-test-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhellothisisflo%2Fappdaemon-test-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhellothisisflo%2Fappdaemon-test-framework/lists"}